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:
2026-02-01 19:57:08 -05:00
commit b374c3b93e
53 changed files with 9482 additions and 0 deletions

47
frontend/.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"printWidth": 80
}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

50
frontend/package.json Normal file
View 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
View 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

View 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();

View 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();

View 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();

View 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
View 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
View 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();
};

View 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();
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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';

View File

@@ -0,0 +1,10 @@
export interface LoginRequest {
username: string;
password: string;
}
export interface LoginResponse {
success: boolean;
message?: string;
token?: string;
}

View 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
View 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>
);

View 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;
}

View 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;
}
}

View 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
View 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;
}

View 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
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View 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
View 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'),
},
},
})