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