Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# SecureCoder
23 changes: 23 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "secure-coder-backend",
"private": true,
"dependencies": {
"@prisma/client": "^6.9.0",
"axios": "^1.9.0",
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^4.21.2"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
"devDependencies": {
"@types/cors": "^2.8.18",
"@types/express": "^4.17.22",
"@types/node": "^22.15.24",
"prisma": "^6.8.2",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"scripts": {
"dev": "ts-node src/index.ts"
}
}
1,020 changes: 1,020 additions & 0 deletions apps/backend/pnpm-lock.yaml

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions apps/backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import express from 'express';
import cors from 'cors';
import exercisesRouter from './routes/exercises'; // adjust path as needed

const app = express();
app.use(cors());
app.use(express.json());

app.use('/api/exercises', exercisesRouter);

const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
3 changes: 3 additions & 0 deletions apps/backend/src/prisma/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;
89 changes: 89 additions & 0 deletions apps/backend/src/routes/exercises.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Router, Request, Response } from 'express';
import axios from "axios";

import prisma from '../prisma/client';
import { buildSubmission, submitToJudge0 } from '../services/judge0client';

const router = Router();

router.get('/', async (req: Request, res: Response) => {
const exercises = await prisma.exercise.findMany();
res.json(exercises);
});

router.post('/', async (req, res) => {
const newexercise = await prisma.exercise.create({
data: req.body
});
res.status(201).json(newexercise);
});

router.get('/:id', async (req: Request, res: Response) => {
const { id } = req.params;
const exercise = await prisma.exercise.findUnique({ where: { id } });
if (!exercise) return res.status(404).json({ error: 'Not found' });
res.json(exercise);
});

router.put('/:id', async (req: Request, res: Response) => {
const { id } = req.params;
try {
const updated = await prisma.exercise.update({
where: { id },
data: req.body,
});
res.json(updated);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to update exercise' });
}
});

router.delete('/:id', async (req: Request, res: Response) => {
const { id } = req.params;

try {
await prisma.exercise.delete({ where: { id } });
res.status(204).send(); // No content on successful delete
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to delete exercise' });
}
});

router.post('/:id/submissions', async (req: Request, res: Response) => {
const { id } = req.params;
let { input } = req.body;

const exercise = await prisma.exercise.findUnique({ where: { id } });
if (!exercise) return res.status(404).json({ error: 'Not found' });

const source = exercise.type === 'offensive' ? exercise.vulnerableCode : input;
input = exercise.type === 'offensive' ? input : null;

try {
const submission = await buildSubmission(source, exercise.driverCode, input);
const result = await submitToJudge0(submission);
res.json(result);
} catch (error) {
console.error('Error submitting or fetching code:', error);
res.status(500).json({ stderr: 'Failed to execute code' });
}
});

router.post('/validate', async (req: Request, res: Response) => {
const { type, driver, vulnerableCode, solution } = req.body;
const source = type === 'offensive' ? vulnerableCode : solution;
const input = type === 'offensive' ? solution : null;

try {
const submission = await buildSubmission(source, driver, input);
const result = await submitToJudge0(submission);
res.json(result);
} catch (error) {
console.error('Error submitting or fetching code:', error);
res.status(500).json({ stderr: 'Failed to execute code' });
}
});

export default router;
81 changes: 81 additions & 0 deletions apps/backend/src/services/judge0client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import axios from "axios";

enum Judge0Status {
InQueue = 1,
Processing,
Accepted,
WrongAnswer,
TimeLimitExceeded,
CompilationError,
RuntimeErrorSIGSEGV,
RuntimeErrorSIGXFSZ,
RuntimeErrorSIGFPE,
RuntimeErrorSIGABRT,
RuntimeErrorNZEC,
RuntimeErrorOther,
InternalError,
ExecFormatError
}

export interface Judge0ApiRequest {
source_code: string,
language_id: number,
stdin: string,
command_line_arguments: string,
compiler_options: string
}

export interface Judge0ApiResponse {
status: number,
feedback?: string,
compile_output?: string,
}

const api = axios.create({
baseURL: process.env.JUDGE0_URL || 'http://localhost:2358',
});

function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export async function buildSubmission(source: string, driver: string, input: string | null): Promise<Judge0ApiRequest> {
return {
source_code: btoa(source + '\n' + driver),
language_id: 50,
stdin: input ? btoa(input) : '',
command_line_arguments: input ?? '',
compiler_options: '-fstack-protector-all',
};
}

export async function submitToJudge0(payload: Judge0ApiRequest): Promise<Judge0ApiResponse> {
const submissionResponse = await api.post('/submissions?base64_encoded=true', payload);
const submissionToken = submissionResponse.data.token;

let result;
let status: number | undefined;

while (true) {
result = await api.get(`/submissions/${submissionToken}?base64_encoded=true`);
status = result.data.status?.id;
if (status === Judge0Status.InQueue || status === Judge0Status.Processing) {
await sleep(1000);
} else {
break;
}
}

const response: Judge0ApiResponse = {
status: status ?? Judge0Status.InternalError,
compile_output: result.data.compile_output ? atob(result.data.compile_output) : '',
};

if (status === Judge0Status.CompilationError) {
response.feedback = 'Compilation error: ' + response.compile_output;
} else if (result.data.stderr) {
response.feedback = atob(result.data.stderr);
}

return response;
}
16 changes: 16 additions & 0 deletions apps/backend/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"paths": {
"@prisma/client": [
"../../node_modules/@prisma/client"
]
}
}
}
28 changes: 28 additions & 0 deletions apps/frontend/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
15 changes: 15 additions & 0 deletions apps/frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SecureCoder: Secure Programming Exercises</title>
</head>

<body class="dark">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
44 changes: 44 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "secure-coder-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.1.8",
"axios": "^1.9.0",
"framer-motion": "^12.15.0",
"monaco-editor": "^0.52.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hook-form": "^7.56.4",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-monaco-editor": "^0.58.0",
"react-router-dom": "^6.30.1"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"postcss": "^8.5.4",
"tailwindcss": "^3.4.17",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.5"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}
6 changes: 6 additions & 0 deletions apps/frontend/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
1 change: 1 addition & 0 deletions apps/frontend/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading