Adding the project for code injection and XSS vulnerability testing
This project is designed to help developers understand and mitigate code injection and XSS vulnerabilities. It includes a backend API and a frontend interface for testing various attack vectors in a controlled environment.
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Codetyper.nvim - AI coding partner files
|
||||
*.coder.*
|
||||
.coder/
|
||||
.claude/
|
||||
.codetyper/
|
||||
**/*.log
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Carlos Gutierrez
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
125
README.md
Normal file
125
README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Secure Software Development Code Injection and XSS practices
|
||||
|
||||
This project is with aiming to help developers understand and mitigate code injection and cross-site scripting (XSS) vulnerabilities in their applications. It provides best practices, examples, and tools to enhance the security of software development.
|
||||
|
||||
## Project Structure
|
||||
|
||||
.
|
||||
├── backend
|
||||
│ └── src
|
||||
│ ├── api
|
||||
│ │ ├── controller
|
||||
│ │ │ ├── controller.js
|
||||
│ │ │ └── secureController.js
|
||||
│ │ └── network
|
||||
│ │ ├── network.js
|
||||
│ │ └── secureNetwork.js
|
||||
│ ├── config
|
||||
│ │ └── config.js
|
||||
│ ├── index.js
|
||||
│ ├── query
|
||||
│ │ ├── database.js
|
||||
│ │ └── secureDatabase.js
|
||||
│ └── routes
|
||||
│ └── routes.js
|
||||
├── frontend
|
||||
│ ├── index.html
|
||||
│ ├── src
|
||||
│ │ ├── api
|
||||
│ │ │ ├── auth.ts
|
||||
│ │ │ └── playground.ts
|
||||
│ │ ├── App.tsx
|
||||
│ │ ├── assets
|
||||
│ │ │ └── logo.png
|
||||
│ │ ├── components
|
||||
│ │ │ ├── atoms
|
||||
│ │ │ │ ├── InputField.tsx
|
||||
│ │ │ │ ├── PasswordField.tsx
|
||||
│ │ │ │ └── SubmitButton.tsx
|
||||
│ │ │ ├── molecules
|
||||
│ │ │ │ ├── EvalPlayground.tsx
|
||||
│ │ │ │ └── LoginFormFields.tsx
|
||||
│ │ │ ├── organisms
|
||||
│ │ │ │ └── LoginForm.tsx
|
||||
│ │ │ └── pages
|
||||
│ │ │ ├── CodePlayground.tsx
|
||||
│ │ │ ├── Header.tsx
|
||||
│ │ │ └── Login.tsx
|
||||
│ │ ├── constants
|
||||
│ │ │ └── app.ts
|
||||
│ │ ├── interfaces
|
||||
│ │ │ ├── auth.ts
|
||||
│ │ │ └── playground.ts
|
||||
│ │ ├── main.tsx
|
||||
│ │ ├── styles
|
||||
│ │ │ ├── App.module.scss
|
||||
│ │ │ ├── Header.module.scss
|
||||
│ │ │ └── Login.module.scss
|
||||
└── └── └── vite-env.d.ts
|
||||
|
||||
## Endpoints
|
||||
|
||||
The backend exposes the following endpoints:
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|-----------------------------------------------|------------------------------------|
|
||||
| GET | / | Home endpoint |
|
||||
| POST | /api/login | SQL Injection vulnerable login endpoint |
|
||||
| POST | /api/secure/login | Secure login endpoint preventing SQL Injection |
|
||||
| POST | /api/execute | eval() vulnerable code execution endpoint |
|
||||
| POST | /api/secure/execute | Secure code execution endpoint preventing code injection |
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js
|
||||
- npm or yarn
|
||||
- A database (PostgreSQL)
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/CarGDev/CodeInjectionAssigment
|
||||
cd CodeInjectionAssigment
|
||||
```
|
||||
|
||||
2. Install backend dependencies:
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Install frontend dependencies:
|
||||
```bash
|
||||
cd ../frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### Running the Application
|
||||
|
||||
1. Start the backend server:
|
||||
```bash
|
||||
cd backend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. Start the frontend development server:
|
||||
```bash
|
||||
cd ../frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. Open your browser and navigate to `http://localhost:5173` to access the application.
|
||||
|
||||
|
||||
## Security Practices
|
||||
|
||||
The project implements the following security practices to mitigate code injection and XSS vulnerabilities:
|
||||
|
||||
- **Parameterized Queries**: All database queries use parameterized statements to prevent SQL injection attacks.
|
||||
- **Input Validation and Sanitization**: User inputs are validated and sanitized to ensure they do not contain malicious code.
|
||||
- **Avoiding eval()**: The playground feature is sanitized to prevent the execution of arbitrary code.
|
||||
|
||||
3
backend/.env.example
Normal file
3
backend/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
PORT=3000
|
||||
DB_HOST=localhost
|
||||
DB_NAME=injection_demo
|
||||
73
backend/.gitignore
vendored
Normal file
73
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Optional npm cache
|
||||
.npm
|
||||
.eslintcache
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
public/
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
|
||||
# Nuxt
|
||||
.nuxt/
|
||||
|
||||
# Gatsby
|
||||
.cache/
|
||||
|
||||
# Parcel
|
||||
.parcel-cache/
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.*.local
|
||||
.env.local
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.vscode/
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Misc
|
||||
*.tgz
|
||||
*.zip
|
||||
*.log.*
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Codetyper.nvim - AI coding partner files
|
||||
*.coder.*
|
||||
.coder/
|
||||
.claude/
|
||||
1365
backend/package-lock.json
generated
Normal file
1365
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
backend/package.json
Normal file
24
backend/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "codeinjection",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Carlos Gutierrez <ingecarlos.gutierrez@gmail.com>",
|
||||
"license": "MIT",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"pg": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.11"
|
||||
}
|
||||
}
|
||||
16
backend/src/api/controller/controller.js
Normal file
16
backend/src/api/controller/controller.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const db = require("../../query/database");
|
||||
|
||||
const loginUser = async (username, password) => {
|
||||
const user = await db.loginUser(username, password);
|
||||
return user;
|
||||
};
|
||||
|
||||
const executeJS = (code) => {
|
||||
const result = db.executeCode(code);
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
loginUser,
|
||||
executeJS,
|
||||
};
|
||||
34
backend/src/api/controller/secureController.js
Normal file
34
backend/src/api/controller/secureController.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const db = require("../../query/secureDatabase");
|
||||
|
||||
const loginUser = async (username, password) => {
|
||||
const userPassword = await db.getUserPassword(username);
|
||||
|
||||
// Compare password with database password
|
||||
if (!userPassword[0] || userPassword[0].password !== password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await db.loginUser(username);
|
||||
|
||||
// Return only the first object with username and email
|
||||
if (user[0]) {
|
||||
return { username: user[0].username, email: user[0].email };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const executeSQL = async (query) => {
|
||||
const result = await db.executeQuery(query);
|
||||
return result;
|
||||
};
|
||||
|
||||
const executeJS = (code) => {
|
||||
const result = db.executeCode(code);
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
loginUser,
|
||||
executeSQL,
|
||||
executeJS,
|
||||
};
|
||||
32
backend/src/api/network/network.js
Normal file
32
backend/src/api/network/network.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const controller = require("../controller/controller");
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const loginUser = async (req, res) => {
|
||||
try {
|
||||
const { username, password } = { ...req.query, ...req.body };
|
||||
const users = await controller.loginUser(username, password);
|
||||
if (users && users.length > 0) {
|
||||
res.json({ success: true, users });
|
||||
} else {
|
||||
res.status(401).json({ success: false, message: "Invalid credentials" });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
const executeCode = async (req, res) => {
|
||||
try {
|
||||
const { code } = { ...req.query, ...req.body };
|
||||
const result = controller.executeJS(code);
|
||||
res.json({ success: true, type: "js", data: result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
router.post("/login", loginUser);
|
||||
router.post("/execute", executeCode);
|
||||
|
||||
module.exports = router;
|
||||
47
backend/src/api/network/secureNetwork.js
Normal file
47
backend/src/api/network/secureNetwork.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const controller = require("../controller/secureController");
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
|
||||
const loginUser = async (req, res) => {
|
||||
try {
|
||||
const { username, password } = { ...req.body };
|
||||
const users = await controller.loginUser(username, password);
|
||||
console.log("Login attempt for user:", users);
|
||||
if (users) {
|
||||
res.json({ success: true, users });
|
||||
} else {
|
||||
res.status(401).json({ success: false, message: "Invalid credentials" });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
const executeCode = async (req, res) => {
|
||||
try {
|
||||
const { code, type } = { ...req.query, ...req.body };
|
||||
if (type === "sql") {
|
||||
const result = await controller.executeSQL(code);
|
||||
res.json({ success: true, type: "sql", data: result });
|
||||
} else if (type === "js") {
|
||||
const result = controller.executeJS(code);
|
||||
res.json({ success: true, type: "js", data: result });
|
||||
} else {
|
||||
// No type specified - try eval first, then SQL
|
||||
try {
|
||||
const result = controller.executeJS(code);
|
||||
res.json({ success: true, type: "js", data: result });
|
||||
} catch {
|
||||
const result = await controller.executeSQL(code);
|
||||
res.json({ success: true, type: "sql", data: result });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
router.post("/login", loginUser);
|
||||
router.post("/execute", executeCode);
|
||||
|
||||
module.exports = router;
|
||||
12
backend/src/config/config.js
Normal file
12
backend/src/config/config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
require("dotenv").config();
|
||||
|
||||
module.exports = {
|
||||
port: process.env.PORT || 3000,
|
||||
db: {
|
||||
host: process.env.DB_HOST || "localhost",
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD || "",
|
||||
},
|
||||
};
|
||||
34
backend/src/index.js
Normal file
34
backend/src/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const express = require("express");
|
||||
const config = require("./config/config");
|
||||
const routes = require("./routes/routes");
|
||||
const cors = require("cors");
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.use("", routes(app));
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.send("Welcome to the API server!");
|
||||
});
|
||||
|
||||
app.listen(config.port, () => {
|
||||
console.log(`Server running on http://localhost:${config.port}`);
|
||||
console.log("\nAvailable endpoints:");
|
||||
console.log(` GET http://localhost:${config.port}/`);
|
||||
console.log(
|
||||
` GET/POST http://localhost:${config.port}/api/login - SQL Injection`,
|
||||
);
|
||||
console.log(
|
||||
` GET/POST http://localhost:${config.port}/api/secure/login - Invalid SQL Injection`,
|
||||
);
|
||||
console.log(
|
||||
` GET/POST http://localhost:${config.port}/api/execute - SQL + eval() (type: sql|js)`,
|
||||
);
|
||||
console.log(
|
||||
` GET/POST http://localhost:${config.port}/api/secure/execute - Invalid SQL + eval() (type: sql|js)`,
|
||||
);
|
||||
});
|
||||
27
backend/src/query/database.js
Normal file
27
backend/src/query/database.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const { Pool } = require("pg");
|
||||
const config = require("../config/config");
|
||||
|
||||
const pool = new Pool(config.db);
|
||||
|
||||
const loginUser = async (username, password) => {
|
||||
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
|
||||
const result = await pool.query(query);
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
const executeQuery = async (query) => {
|
||||
const result = await pool.query(query);
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
const executeCode = (code) => {
|
||||
console.log("Executing code:", code);
|
||||
const result = eval(code);
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
loginUser,
|
||||
executeQuery,
|
||||
executeCode,
|
||||
};
|
||||
90
backend/src/query/secureDatabase.js
Normal file
90
backend/src/query/secureDatabase.js
Normal file
@@ -0,0 +1,90 @@
|
||||
const { Pool } = require("pg");
|
||||
const config = require("../config/config");
|
||||
|
||||
const pool = new Pool(config.db);
|
||||
|
||||
// Regex pattern matching dangerous SQL characters
|
||||
const DANGEROUS_CHARS = /['";\\`\-\-\/\*\+\=\(\)\[\]\{\}\|\&\^\%\$\#\@\!\~]/g;
|
||||
|
||||
// Sanitize input by removing dangerous characters
|
||||
const sanitizeInput = (input) => {
|
||||
if (typeof input !== "string") {
|
||||
return String(input);
|
||||
}
|
||||
return input.replace(DANGEROUS_CHARS, "");
|
||||
};
|
||||
|
||||
// Secure parameterized query - equivalent to Java PreparedStatement
|
||||
const getUserByUsername = async (username) => {
|
||||
const sql = "SELECT * FROM users WHERE username = $1";
|
||||
const result = await pool.query(sql, [username]); // Parameter is safely escaped
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
const getUserPassword = async (username) => {
|
||||
let secureUsername = sanitizeInput(username);
|
||||
const sql = "SELECT password FROM users WHERE username = $1";
|
||||
const result = await pool.query(sql, [secureUsername]); // Parameter is safely escaped
|
||||
console.log("Retrieved password for user:", secureUsername);
|
||||
return result.rows;
|
||||
};
|
||||
|
||||
const loginUser = async (username) => {
|
||||
let secureUsername = sanitizeInput(username);
|
||||
return await getUserByUsername(secureUsername);
|
||||
};
|
||||
|
||||
const executeQuery = async (query) => {
|
||||
let secureQuery = sanitizeInput(query);
|
||||
const result = await pool.query(secureQuery);
|
||||
console.log("Attempting login for user:", result);
|
||||
const sanitizedRows = result.rows.map((row) => {
|
||||
const rowKeys = Object.keys(row);
|
||||
if (rowKeys.includes("username") && rowKeys.includes("email")) {
|
||||
return { username: row.username, email: row.email };
|
||||
}
|
||||
});
|
||||
return sanitizedRows;
|
||||
};
|
||||
|
||||
const DANGEROUS_JS_PATTERNS = [
|
||||
/require\s*\(/gi,
|
||||
/import\s*\(/gi,
|
||||
/process\./gi,
|
||||
/child_process/gi,
|
||||
/fs\./gi,
|
||||
/eval\s*\(/gi,
|
||||
/Function\s*\(/gi,
|
||||
/exec\s*\(/gi,
|
||||
/spawn\s*\(/gi,
|
||||
/__dirname/gi,
|
||||
/__filename/gi,
|
||||
/global\./gi,
|
||||
];
|
||||
|
||||
const sanitizeCode = (code) => {
|
||||
if (typeof code !== "string") {
|
||||
return String(code);
|
||||
}
|
||||
// Only use pattern detection for JS code - don't strip characters
|
||||
// This allows safe operations like math, string methods, and JSON
|
||||
for (const pattern of DANGEROUS_JS_PATTERNS) {
|
||||
if (pattern.test(code)) {
|
||||
throw new Error("Dangerous code pattern detected");
|
||||
}
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
||||
const executeCode = (code) => {
|
||||
let secureCode = sanitizeCode(code);
|
||||
const result = eval(secureCode);
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
loginUser,
|
||||
executeQuery,
|
||||
executeCode,
|
||||
getUserPassword,
|
||||
};
|
||||
10
backend/src/routes/routes.js
Normal file
10
backend/src/routes/routes.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const network = require("../api/network/network");
|
||||
const secureNetwork = require("../api/network/secureNetwork");
|
||||
|
||||
const routes = (app) => {
|
||||
app.use("/api", network);
|
||||
app.use("/api/secure", secureNetwork);
|
||||
return network;
|
||||
};
|
||||
|
||||
module.exports = routes;
|
||||
47
frontend/.gitignore
vendored
Normal file
47
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# React & Node.js
|
||||
# Node dependencies
|
||||
node_modules/
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
|
||||
# Build outputs
|
||||
/build
|
||||
/dist
|
||||
/dist-ssr
|
||||
/.next
|
||||
/out
|
||||
/.parcel-cache
|
||||
/public/build
|
||||
|
||||
# Local environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Testing
|
||||
/coverage
|
||||
coverage/*
|
||||
/.nyc_output
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
node_modules/
|
||||
7
frontend/.prettierrc
Normal file
7
frontend/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"printWidth": 80
|
||||
}
|
||||
22
frontend/commitlint.config.js
Normal file
22
frontend/commitlint.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat',
|
||||
'fix',
|
||||
'docs',
|
||||
'style',
|
||||
'refactor',
|
||||
'test',
|
||||
'chore',
|
||||
'perf',
|
||||
'ci',
|
||||
'build',
|
||||
'revert',
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
23
frontend/eslint.config.js
Normal file
23
frontend/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
6489
frontend/package-lock.json
generated
Normal file
6489
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
frontend/package.json
Normal file
50
frontend/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"pretty-quick": "pretty-quick",
|
||||
"lint:prettier": "npx ts-node scripts/check-format.ts",
|
||||
"prettier": "prettier --write . --config .prettierrc",
|
||||
"prettier:commit": "npx ts-node scripts/prettier-commit.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@commitlint/cli": "^20.4.0",
|
||||
"@commitlint/config-conventional": "^20.4.0",
|
||||
"@originjs/vite-plugin-federation": "^1.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/jest": "^30.0.0",
|
||||
"antd": "^6.2.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"ora": "^9.1.0",
|
||||
"prettier": "^3.8.1",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"sass": "^1.97.3",
|
||||
"web-vitals": "^5.1.0",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.24",
|
||||
"globals": "^16.5.0",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
23
frontend/scripts/check-format.ts
Normal file
23
frontend/scripts/check-format.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import ora from 'ora';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
async function run() {
|
||||
const spinner = ora('Checking code formatting...').start();
|
||||
|
||||
try {
|
||||
const { stdout } = await execPromise(
|
||||
'npm run pretty-quick --check . --config .prettierrc'
|
||||
);
|
||||
spinner.succeed('Code formatting check passed.');
|
||||
console.log(stdout);
|
||||
} catch (error: any) {
|
||||
spinner.fail('Code formatting check failed.');
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
156
frontend/scripts/commit-msg-linter.ts
Normal file
156
frontend/scripts/commit-msg-linter.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { exec, execSync } from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
const commitTypes: Record<string, string> = {
|
||||
feat: '✨',
|
||||
fix: '🐛',
|
||||
docs: '📚',
|
||||
style: '🎨',
|
||||
refactor: '🔨',
|
||||
test: '✅',
|
||||
chore: '🛠️',
|
||||
perf: '⚡',
|
||||
ci: '🔧',
|
||||
build: '📦',
|
||||
revert: '⏪',
|
||||
};
|
||||
|
||||
const defaultEmoji = '🔖';
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const spinner = ora('Running custom commit message check...').start();
|
||||
|
||||
try {
|
||||
console.log(chalk.blue('Running custom commit message check...'));
|
||||
console.log();
|
||||
|
||||
const commitMsgFile = process.argv[2];
|
||||
|
||||
if (!commitMsgFile) {
|
||||
spinner.fail('Error: Commit message file path not provided.');
|
||||
console.error(chalk.red('Error: Commit message file path not provided.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const commitMsg = (await fs.readFile(commitMsgFile, 'utf8')).trim();
|
||||
|
||||
// Check for duplicate commit messages in the last 100 commits
|
||||
const duplicateCommitMsg = execSync(`git log -n 100 --pretty=format:%s`)
|
||||
.toString()
|
||||
.split('\n');
|
||||
|
||||
// Extract emojis from commitTypes
|
||||
const emojis = Object.values(commitTypes);
|
||||
|
||||
// Function to remove an emoji from the start of the string
|
||||
const removeEmoji = (message: string): string => {
|
||||
for (const emoji of emojis) {
|
||||
if (message.startsWith(emoji)) {
|
||||
return message.slice(emoji.length).trim();
|
||||
}
|
||||
}
|
||||
if (message.startsWith(defaultEmoji)) {
|
||||
return message.slice(defaultEmoji.length).trim();
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
const cleanMessages = duplicateCommitMsg.map(removeEmoji);
|
||||
|
||||
if (cleanMessages.includes(commitMsg)) {
|
||||
spinner.fail(chalk.bold.red('Duplicate Commit Detected'));
|
||||
console.log();
|
||||
console.error(
|
||||
chalk.white.bgRed.bold(' ERROR: ') +
|
||||
chalk.redBright(' A duplicate commit message has been detected.')
|
||||
);
|
||||
console.log();
|
||||
console.log(
|
||||
chalk.yellowBright('TIP: ') +
|
||||
chalk.white(' Please use a unique commit message to keep the history clean.')
|
||||
);
|
||||
console.log();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner.succeed('Message is not duplicated');
|
||||
console.log(chalk.green('Message is not duplicated'));
|
||||
console.log();
|
||||
} catch (err) {
|
||||
spinner.fail('Error running custom commit message check.');
|
||||
console.error(chalk.red('Error:', err));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spinner2 = ora('Running commitlint...').start();
|
||||
|
||||
try {
|
||||
console.log(chalk.blue('Running commitlint...'));
|
||||
console.log();
|
||||
|
||||
const commitMsgFile = process.argv[2];
|
||||
|
||||
if (!commitMsgFile) {
|
||||
spinner2.fail('Error: Commit message file path not provided.');
|
||||
console.error(chalk.red('Error: Commit message file path not provided.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const commitMsg = (await fs.readFile(commitMsgFile, 'utf8')).trim();
|
||||
|
||||
// Run commitlint
|
||||
exec(
|
||||
`npx commitlint --edit ${commitMsgFile}`,
|
||||
async (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
spinner2.fail('Commitlint check failed.');
|
||||
console.error(chalk.red(stdout || stderr));
|
||||
console.error(chalk.red('Commitlint check failed.'));
|
||||
console.log();
|
||||
console.error(
|
||||
chalk.yellow('Hint: Commit message should follow the Conventional Commits standard.')
|
||||
);
|
||||
console.error(chalk.yellow('See: https://www.conventionalcommits.org/en/v1.0.0/'));
|
||||
console.error(chalk.yellow('Examples:'));
|
||||
console.error(chalk.yellow(' feat: add a new feature'));
|
||||
console.error(chalk.yellow(' fix: fix a bug'));
|
||||
console.error(chalk.yellow(' docs: update documentation'));
|
||||
process.exit(1);
|
||||
} else {
|
||||
spinner2.succeed('Commitlint check passed.');
|
||||
console.log(chalk.green('Commitlint check passed.'));
|
||||
console.log(chalk.green(stdout));
|
||||
|
||||
// Add emoji to the commit message
|
||||
const commitRegex = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?:\s.+/;
|
||||
const match = commitMsg.match(commitRegex);
|
||||
|
||||
if (match) {
|
||||
const commitType = match[1];
|
||||
const emoji = commitTypes[commitType] || defaultEmoji;
|
||||
const newCommitMsg = `${emoji} ${commitMsg}`;
|
||||
await fs.writeFile(commitMsgFile, newCommitMsg + '\n', 'utf8');
|
||||
console.log(chalk.green('Commit message updated with emoji:'), newCommitMsg);
|
||||
} else {
|
||||
const newCommitMsg = `${defaultEmoji} ${commitMsg}`;
|
||||
await fs.writeFile(commitMsgFile, newCommitMsg + '\n', 'utf8');
|
||||
console.log(
|
||||
chalk.yellow('Commit message did not match expected format, added default emoji:'),
|
||||
newCommitMsg
|
||||
);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
spinner2.fail('Error running commitlint.');
|
||||
console.error(chalk.red('Error:', err));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
42
frontend/scripts/lint-check.ts
Normal file
42
frontend/scripts/lint-check.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { exec } from 'child_process';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
async function runCommand(command: string, description: string): Promise<void> {
|
||||
const spinner = ora(`Running ${description}...`).start();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
spinner.fail(`${description} failed.`);
|
||||
console.error(chalk.red(`${description} failed.`));
|
||||
console.error(chalk.red(stderr));
|
||||
reject(new Error(stderr));
|
||||
} else {
|
||||
spinner.succeed(`${description} passed.`);
|
||||
console.log(chalk.green(`${description} passed.`));
|
||||
console.log(stdout);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runLint(): Promise<void> {
|
||||
try {
|
||||
await runCommand('npm run lint:prettier', 'Prettier check');
|
||||
console.log(chalk.green('All checks passed.'));
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error(chalk.red('Lint checks failed.'));
|
||||
console.error(chalk.red('Please fix the issues above and try again.'));
|
||||
console.error(
|
||||
chalk.yellow(
|
||||
`Hint: You can run ${chalk.cyan('npm run prettier')} to automatically format your code.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runLint();
|
||||
47
frontend/scripts/prettier-commit.ts
Normal file
47
frontend/scripts/prettier-commit.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { execSync } from 'child_process';
|
||||
import ora from 'ora';
|
||||
|
||||
async function run(): Promise<void> {
|
||||
const spinner = ora('Running Prettier...').start();
|
||||
|
||||
try {
|
||||
// Run Prettier
|
||||
execSync('npm run prettier', { stdio: 'inherit' });
|
||||
spinner.succeed('Prettier has formatted the files.');
|
||||
|
||||
// Check for changes
|
||||
spinner.start('Checking for changes...');
|
||||
const changes = execSync('git status --porcelain', { encoding: 'utf-8' });
|
||||
|
||||
if (changes) {
|
||||
spinner.succeed('Changes detected.');
|
||||
// Read the latest commit message to ensure uniqueness
|
||||
const latestCommitMessage = execSync(`git log -n 100 --pretty=format:%s`)
|
||||
.toString()
|
||||
.split('\n');
|
||||
|
||||
// Generate a unique commit message
|
||||
let commitMessage = 'style: format with prettier';
|
||||
if (latestCommitMessage.includes(commitMessage)) {
|
||||
commitMessage = `style: format with prettier ${Date.now()}`;
|
||||
}
|
||||
|
||||
// Add and commit changes
|
||||
spinner.start('Adding changes to Git...');
|
||||
execSync('git add .', { stdio: 'inherit' });
|
||||
spinner.succeed('Changes added to Git.');
|
||||
|
||||
spinner.start('Committing changes...');
|
||||
execSync(`git commit -m "${commitMessage}"`, { stdio: 'inherit' });
|
||||
spinner.succeed('Changes committed.');
|
||||
} else {
|
||||
spinner.info('No changes detected by Prettier.');
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail('An error occurred while running Prettier.');
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
39
frontend/src/App.tsx
Normal file
39
frontend/src/App.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Layout } from 'antd';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import styles from '@src/styles/App.module.scss';
|
||||
import CustomHeader from '@src/components/pages/Header';
|
||||
import Login from '@src/components/pages/Login';
|
||||
import CodePlayground from '@src/components/pages/CodePlayground';
|
||||
import {
|
||||
PLAYGROUND_ROUTE,
|
||||
LOGIN_SECURE_ROUTE,
|
||||
PLAYGROUND_SECURE_ROUTE,
|
||||
LOGIN_ROUTE,
|
||||
} from '@src/constants/app';
|
||||
const { Header, Content } = Layout;
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Layout className={styles.layout}>
|
||||
<Header className={styles.headerStyle}>
|
||||
<CustomHeader />
|
||||
</Header>
|
||||
<Content className={styles.contentStyle}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||
<Route path={LOGIN_ROUTE} element={<Login />} />
|
||||
<Route path={LOGIN_SECURE_ROUTE} element={<Login />} />
|
||||
<Route path={PLAYGROUND_ROUTE} element={<CodePlayground />} />
|
||||
<Route
|
||||
path={PLAYGROUND_SECURE_ROUTE}
|
||||
element={<CodePlayground />}
|
||||
/>
|
||||
</Routes>
|
||||
</Content>
|
||||
</Layout>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
22
frontend/src/api/auth.ts
Normal file
22
frontend/src/api/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { LoginRequest, LoginResponse } from '@src/interfaces/auth';
|
||||
import {
|
||||
API_BASE_URL,
|
||||
LOGIN_ENDPOINT,
|
||||
LOGIN_SECURE_ENDPOINT,
|
||||
} from '@src/constants/app';
|
||||
|
||||
export const login = async (
|
||||
credentials: LoginRequest,
|
||||
secure?: boolean
|
||||
): Promise<LoginResponse> => {
|
||||
const endpoint = secure ? `${LOGIN_SECURE_ENDPOINT}` : LOGIN_ENDPOINT;
|
||||
const response = await fetch(`${API_BASE_URL}/api${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
};
|
||||
25
frontend/src/api/playground.ts
Normal file
25
frontend/src/api/playground.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type {
|
||||
ExecuteCodeRequest,
|
||||
ExecuteCodeResponse,
|
||||
} from '@src/interfaces/playground';
|
||||
import {
|
||||
API_BASE_URL,
|
||||
EXECUTE_ENDPOINT,
|
||||
EXECUTE_SECURE_ENDPOINT,
|
||||
} from '@src/constants/app';
|
||||
|
||||
export const executeCode = async (
|
||||
data: ExecuteCodeRequest,
|
||||
secure?: boolean
|
||||
): Promise<ExecuteCodeResponse> => {
|
||||
const endpoint = secure ? `${EXECUTE_SECURE_ENDPOINT}` : EXECUTE_ENDPOINT;
|
||||
const response = await fetch(`${API_BASE_URL}/api${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
};
|
||||
BIN
frontend/src/assets/logo.png
Normal file
BIN
frontend/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 522 KiB |
13
frontend/src/components/atoms/InputField.tsx
Normal file
13
frontend/src/components/atoms/InputField.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Input } from 'antd';
|
||||
import type { InputProps } from 'antd';
|
||||
|
||||
interface InputFieldProps extends InputProps {
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const InputField: React.FC<InputFieldProps> = ({ icon, ...props }) => {
|
||||
return <Input prefix={icon} {...props} />;
|
||||
};
|
||||
|
||||
export default InputField;
|
||||
13
frontend/src/components/atoms/PasswordField.tsx
Normal file
13
frontend/src/components/atoms/PasswordField.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Input } from 'antd';
|
||||
import type { PasswordProps } from 'antd/es/input';
|
||||
|
||||
interface PasswordFieldProps extends PasswordProps {
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const PasswordField: React.FC<PasswordFieldProps> = ({ icon, ...props }) => {
|
||||
return <Input.Password prefix={icon} {...props} />;
|
||||
};
|
||||
|
||||
export default PasswordField;
|
||||
17
frontend/src/components/atoms/SubmitButton.tsx
Normal file
17
frontend/src/components/atoms/SubmitButton.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'antd';
|
||||
import type { ButtonProps } from 'antd';
|
||||
|
||||
interface SubmitButtonProps extends ButtonProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SubmitButton: React.FC<SubmitButtonProps> = ({ children, ...props }) => {
|
||||
return (
|
||||
<Button type="primary" htmlType="submit" block {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubmitButton;
|
||||
75
frontend/src/components/molecules/EvalPlayground.tsx
Normal file
75
frontend/src/components/molecules/EvalPlayground.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Typography, Radio } from 'antd';
|
||||
import { executeCode } from '@src/api/playground';
|
||||
import SubmitButton from '@src/components/atoms/SubmitButton';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import logo from '@assets/logo.png';
|
||||
import styles from '@src/styles/Login.module.scss';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const EvalPlayground: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const isSecure = location.pathname.startsWith('/secure');
|
||||
const [code, setCode] = React.useState<string>('');
|
||||
const [type, setType] = React.useState<'js' | 'sql'>('js');
|
||||
const [response, setResponse] = React.useState<string>('');
|
||||
const [error, setError] = React.useState<string>('');
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
const handleExecute = async () => {
|
||||
setError('');
|
||||
setResponse('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await executeCode({ code }, isSecure);
|
||||
setResponse(JSON.stringify(res, null, 2));
|
||||
} catch {
|
||||
setError('An error occurred while executing code');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.evalPlayground}>
|
||||
<img src={logo} alt="Logo" className={styles.logo} />
|
||||
<Title level={4}>Code Playground</Title>
|
||||
<Text type="secondary">Enter code to execute:</Text>
|
||||
<Form layout="vertical" onFinish={handleExecute}>
|
||||
<Form.Item>
|
||||
<TextArea
|
||||
rows={4}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder={'Enter JavaScript code... e.g., 1 + 1'}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<SubmitButton loading={loading}>Execute</SubmitButton>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{response && (
|
||||
<div className={styles.resultBox}>
|
||||
<Text strong>Response:</Text>
|
||||
<TextArea
|
||||
rows={6}
|
||||
value={response}
|
||||
readOnly
|
||||
style={{ marginTop: 8, fontFamily: 'monospace' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className={styles.errorBox}>
|
||||
<Text type="danger">{error}</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EvalPlayground;
|
||||
26
frontend/src/components/molecules/LoginFormFields.tsx
Normal file
26
frontend/src/components/molecules/LoginFormFields.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Form } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import InputField from '@src/components/atoms/InputField';
|
||||
import PasswordField from '@src/components/atoms/PasswordField';
|
||||
|
||||
const LoginFormFields: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: 'Please enter your username' }]}
|
||||
>
|
||||
<InputField icon={<UserOutlined />} placeholder="Username" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: 'Please enter your password' }]}
|
||||
>
|
||||
<PasswordField icon={<LockOutlined />} placeholder="Password" />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginFormFields;
|
||||
80
frontend/src/components/organisms/LoginForm.tsx
Normal file
80
frontend/src/components/organisms/LoginForm.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { Form, Typography, message } from 'antd';
|
||||
import { login } from '@src/api/auth';
|
||||
import type { LoginRequest } from '@src/interfaces/auth';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import LoginFormFields from '@src/components/molecules/LoginFormFields';
|
||||
import SubmitButton from '@src/components/atoms/SubmitButton';
|
||||
import logo from '@assets/logo.png';
|
||||
import styles from '@src/styles/Login.module.scss';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const LoginForm: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const isSecure = location.pathname.startsWith('/secure');
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [welcomeMessage, setWelcomeMessage] = React.useState<string>('');
|
||||
const [evalResult, setEvalResult] = React.useState<string>('');
|
||||
|
||||
const onFinish = async (values: LoginRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await login(values, isSecure);
|
||||
if (response.success) {
|
||||
message.success('Login successful!');
|
||||
// VULNERABLE: XSS - Directly rendering user input as HTML
|
||||
setWelcomeMessage(`Welcome, ${values.username}!`);
|
||||
|
||||
// VULNERABLE: eval() - Executing dynamic code from response
|
||||
if (response.message) {
|
||||
try {
|
||||
const result = eval(response.message);
|
||||
setEvalResult(String(result));
|
||||
} catch {
|
||||
setEvalResult(response.message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(response.message || 'Login failed');
|
||||
// VULNERABLE: XSS - Rendering error message as HTML
|
||||
setWelcomeMessage(response.message || '');
|
||||
}
|
||||
} catch {
|
||||
message.error('An error occurred during login');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.loginForm}>
|
||||
<img src={logo} alt="Logo" className={styles.logo} />
|
||||
<Title level={3} className={styles.loginTitle}>
|
||||
Login
|
||||
</Title>
|
||||
<Form form={form} name="login" onFinish={onFinish} layout="vertical">
|
||||
<LoginFormFields />
|
||||
<Form.Item>
|
||||
<SubmitButton loading={loading}>Login</SubmitButton>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{/* VULNERABLE: XSS - dangerouslySetInnerHTML renders raw HTML */}
|
||||
{welcomeMessage && (
|
||||
<div
|
||||
className={styles.welcomeMessage}
|
||||
dangerouslySetInnerHTML={{ __html: welcomeMessage }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* VULNERABLE: Displaying eval() result */}
|
||||
{evalResult && (
|
||||
<div className={styles.evalResult}>Eval Result: {evalResult}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
13
frontend/src/components/pages/CodePlayground.tsx
Normal file
13
frontend/src/components/pages/CodePlayground.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import EvalPlayground from '@src/components/molecules/EvalPlayground';
|
||||
import styles from '@src/styles/Login.module.scss';
|
||||
|
||||
const CodePlayground: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.loginContainer}>
|
||||
<EvalPlayground />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodePlayground;
|
||||
43
frontend/src/components/pages/Header.tsx
Normal file
43
frontend/src/components/pages/Header.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import styles from '@src/styles/Header.module.scss';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const pageTitles: Record<string, string> = {
|
||||
'/': 'Super Insecure Login',
|
||||
'/login': 'Super Insecure Login',
|
||||
'/playground': 'Code Playground',
|
||||
'/secure/login': 'Secure Login',
|
||||
'/secure/playground': 'Secure Code Playground',
|
||||
};
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const title = pageTitles[location.pathname] || 'Super Insecure App';
|
||||
|
||||
return (
|
||||
<div className={styles.headerContent}>
|
||||
<Title level={2} className={styles.title}>
|
||||
{title}
|
||||
</Title>
|
||||
<nav className={styles.nav}>
|
||||
<Link to="/login" className={styles.navLink}>
|
||||
Login
|
||||
</Link>
|
||||
<Link to="/playground" className={styles.navLink}>
|
||||
Playground
|
||||
</Link>
|
||||
<Link to="/secure/login" className={styles.navLink}>
|
||||
Login(Secure)
|
||||
</Link>
|
||||
<Link to="/secure/playground" className={styles.navLink}>
|
||||
Playground(Secure)
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
13
frontend/src/components/pages/Login.tsx
Normal file
13
frontend/src/components/pages/Login.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import LoginForm from '@src/components/organisms/LoginForm';
|
||||
import styles from '@src/styles/Login.module.scss';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
return (
|
||||
<div className={styles.loginContainer}>
|
||||
<LoginForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
10
frontend/src/constants/app.ts
Normal file
10
frontend/src/constants/app.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const API_BASE_URL = 'http://localhost:3000';
|
||||
export const LOGIN_ENDPOINT = '/login';
|
||||
export const EXECUTE_ENDPOINT = '/execute';
|
||||
export const PLAYGROUND_ROUTE = '/playground';
|
||||
export const LOGIN_ROUTE = '/login';
|
||||
|
||||
export const LOGIN_SECURE_ENDPOINT = '/secure/login';
|
||||
export const EXECUTE_SECURE_ENDPOINT = '/secure/execute';
|
||||
export const PLAYGROUND_SECURE_ROUTE = '/secure/playground';
|
||||
export const LOGIN_SECURE_ROUTE = '/secure/login';
|
||||
10
frontend/src/interfaces/auth.ts
Normal file
10
frontend/src/interfaces/auth.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
token?: string;
|
||||
}
|
||||
11
frontend/src/interfaces/playground.ts
Normal file
11
frontend/src/interfaces/playground.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface ExecuteCodeRequest {
|
||||
code: string;
|
||||
type?: 'sql' | 'js';
|
||||
}
|
||||
|
||||
export interface ExecuteCodeResponse {
|
||||
success: boolean;
|
||||
type?: string;
|
||||
data?: unknown;
|
||||
message?: string;
|
||||
}
|
||||
9
frontend/src/main.tsx
Normal file
9
frontend/src/main.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
20
frontend/src/styles/App.module.scss
Normal file
20
frontend/src/styles/App.module.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.layout {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.headerStyle {
|
||||
background: linear-gradient(135deg, #0a1628 0%, #1e3a5f 50%, #2563eb 100%);
|
||||
padding: 0;
|
||||
height: auto;
|
||||
min-height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.contentStyle {
|
||||
background: linear-gradient(180deg, #e0f2ff 0%, #bae6fd 50%, #7dd3fc 100%);
|
||||
min-height: calc(100vh - 64px);
|
||||
padding: 24px;
|
||||
}
|
||||
34
frontend/src/styles/Header.module.scss
Normal file
34
frontend/src/styles/Header.module.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
.headerContent {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #ffffff !important;
|
||||
margin: 0 !important;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.navLink {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
71
frontend/src/styles/Login.module.scss
Normal file
71
frontend/src/styles/Login.module.scss
Normal file
@@ -0,0 +1,71 @@
|
||||
.loginContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: calc(100vh - 64px - 48px);
|
||||
padding: 24px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.loginForm {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 16px;
|
||||
max-width: 120px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.loginTitle {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.welcomeMessage {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #f0f5ff;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.evalResult {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.evalPlayground {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.resultBox {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.errorBox {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: #fff2f0;
|
||||
border: 1px solid #ffccc7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
19
frontend/src/vite-env.d.ts
vendored
Normal file
19
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
declare module '*.module.css' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.sass' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.module.less' {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
33
frontend/tsconfig.app.json
Normal file
33
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@src/*": ["src/*"],
|
||||
"@assets/*": ["src/assets/*"]
|
||||
},
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
frontend/tsconfig.node.json
Normal file
26
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
14
frontend/vite.config.ts
Normal file
14
frontend/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@src': path.resolve(__dirname, './src'),
|
||||
'@assets': path.resolve(__dirname, './src/assets'),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user