36 Commits

Author SHA1 Message Date
0e35d086b0 test1 2025-12-05 00:43:36 +05:30
917de3e401 test1 2025-12-05 00:39:46 +05:30
2e089e192d Merge branch 'main' of https://git.csi-kjsce.org/Arnab-Afk/monaco 2025-12-05 00:37:51 +05:30
38cf325e22 gitea test 2025-12-05 00:36:15 +05:30
fff316d5ec test1 2025-12-05 00:34:39 +05:30
16620111cf test 2025-12-04 23:55:16 +05:30
ishikabhoyar
fbbcabbeab style: update padding for console content for improved layout 2025-11-09 13:58:11 +05:30
ishikabhoyar
35d40da727 feat: enhance terminal input handling and improve WebSocket connection safety 2025-11-09 13:52:48 +05:30
ebaef13845 feat: add functionality to mark questions for review and update styles for marked questions 2025-11-08 23:03:25 +05:30
b075e5b3d3 feat: implement test cases tab with styling and functionality for displaying test cases 2025-11-08 22:52:51 +05:30
ishikabhoyar
1d3b1c74e1 fix: process code input to handle newlines and tabs correctly; update CSS for better text wrapping 2025-11-08 18:44:55 +05:30
ishikabhoyar
8ec0935486 refactor question bar and problem container styles for improved layout and readability 2025-11-03 20:21:33 +05:30
ishikabhoyar
33b9e2fe38 add reset code functionality to restore original starter code in CodeChallenge component 2025-11-03 20:02:01 +05:30
ishikabhoyar
c579f972b8 remove example problem data and related starter code handling from CodeChallenge component 2025-11-03 19:59:49 +05:30
ishikabhoyar
0c844c3122 remove starter code templates for all questions in CodeChallenge component 2025-11-03 19:53:45 +05:30
356d532beb add terminal input handling and styling to CodeChallenge component 2025-11-03 15:30:58 +05:30
ishikabhoyar
2508731ec7 refactor WebSocket and fetch API URLs to use environment variables for better configuration 2025-11-03 14:25:01 +05:30
6ef2edb768 add tunnel-only setup with host network mode, including updated Dockerfile, docker-compose, and configuration files 2025-11-01 00:55:04 +05:30
fbafab5d51 add tunnel-only setup with Docker, including Dockerfile, docker-compose, and scripts 2025-11-01 00:49:59 +05:30
a433c5067f update service configuration to use host.docker.internal for backend connectivity 2025-10-31 21:21:46 +05:30
ishikabhoyar
ec33604a6f refactor docker-compose configuration to simplify network setup for tunnel service 2025-10-31 16:36:17 +05:30
ishikabhoyar
dbd8770f20 refactor Dockerfile and configuration for cloudflared tunnel setup 2025-10-31 16:28:37 +05:30
ishikabhoyar
9fa0528ff0 remove DNS routing setup for cloudflared tunnel in Dockerfile 2025-10-31 16:23:19 +05:30
ishikabhoyar
e82554215b add Dockerfile, docker-compose, and scripts for frontend tunnel setup 2025-10-31 16:08:02 +05:30
ishikabhoyar
abc15efabd refactor API URL handling in TestList and AuthContext components 2025-10-31 15:12:58 +05:30
ishikabhoyar
b18dc5f21b update styling for language selector and reset code button in CodeChallenge component 2025-10-30 23:24:48 +05:30
ishikabhoyar
336ad17240 remove active line border from Monaco Editor 2025-10-30 23:10:43 +05:30
ishikabhoyar
9d180e13b1 increase font size for time labels in index.css 2025-10-30 23:07:04 +05:30
ishikabhoyar
6b98938245 update font sizes and layout adjustments in CodeChallenge component 2025-10-30 23:06:22 +05:30
ishikabhoyar
9a1dee00a1 Enhance UI and UX of Code Challenge Component 2025-10-30 22:21:49 +05:30
ishikabhoyar
9d6729e63c modernize UI for Code Challenge page with new header, question palette, and improved styling 2025-10-30 21:54:07 +05:30
ishikabhoyar
4daafa726d fix: update API URL for student submissions in CodeChallenge component 2025-10-29 16:43:37 +05:30
47f73681af feat: Update login navigation and authentication flow
- Changed navigation from '/editor' to '/tests' after successful login.
- Introduced token state management in AuthContext for better handling of authentication.
- Updated login function to store JWT instead of Google token.
- Added error handling for login and test fetching processes.

style: Enhance UI with new footer and test list styles

- Added a footer component with copyright information.
- Created a new TestList component with improved styling and animations.
- Implemented responsive design for test cards and filter tabs.
- Added loading and error states for better user experience.

fix: Improve API interaction for test fetching and password verification

- Refactored API calls to use a centralized studentApi utility.
- Enhanced error handling for API responses, including unauthorized access.
- Implemented password verification for protected tests before starting them.
2025-10-29 11:37:19 +05:30
ishikabhoyar
304761e258 Remove Header component from ProtectedRoute in App.jsx 2025-10-28 14:14:44 +05:30
ishikabhoyar
453f44a43a Add Google OAuth integration, routing, and theme toggle; implement login and protected routes 2025-10-13 23:51:39 +05:30
Ishika Bhoyar
e8e6011524 Merge pull request #2 from ishikabhoyar/leetcode
Leetcode
2025-10-13 16:38:55 +05:30
44 changed files with 4210 additions and 666 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1 +1,2 @@
VITE_API_URL="http://localhost:8080" VITE_API_URL="http://localhost:8080"
VITE_FACULTY_API_URL="http://localhost:5000/api"

7
Frontend/.env.example Normal file
View File

@@ -0,0 +1,7 @@
# Monaco Frontend Environment Variables
# Backend API URL (Monaco code execution server)
VITE_API_URL=http://localhost:8080
# Faculty API URL (Faculty/Student management backend)
VITE_FACULTY_API_URL=http://localhost:5000/api

View File

@@ -0,0 +1,35 @@
# Final stage
FROM alpine:latest
# Install supervisor and wget
RUN apk update && apk add --no-cache supervisor wget
# Get cloudflared directly from GitHub
RUN wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/download/2023.5.0/cloudflared-linux-amd64 && \
chmod +x cloudflared && \
mv cloudflared /usr/local/bin/
# Create directories for cloudflared
RUN mkdir -p /etc/cloudflared
# Copy the cloudflared config and credentials
COPY credentials.json /etc/cloudflared/credentials.json
COPY config.json /etc/cloudflared/config.json
# Create supervisord config (only cloudflared, no frontend inside container)
RUN mkdir -p /etc/supervisor/conf.d/
RUN echo "[supervisord]" > /etc/supervisor/conf.d/supervisord.conf && \
echo "nodaemon=true" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "user=root" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "[program:cloudflared]" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "command=cloudflared tunnel --config /etc/cloudflared/config.json run" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "autostart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "autorestart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf
# Use supervisord to manage cloudflared
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

15
Frontend/config.json Normal file
View File

@@ -0,0 +1,15 @@
{
"tunnel": "8c559c7c-42bb-4b9d-96a2-99cefd274b06",
"credentials-file": "/etc/cloudflared/credentials.json",
"ingress": [
{
"hostname": "monaco.ishikabhoyar.tech",
"service": "http://host.docker.internal:8001"
},
{
"service": "http_status:404"
}
],
"protocol": "http2",
"loglevel": "info"
}

View File

@@ -0,0 +1,5 @@
{
"AccountTag": "453afb9373a00a55876e6127cf0efd97",
"TunnelSecret": "afJ6YY25rj9+G6qqHy+2jss4h+QKfw6YntijRZvo4ZQ=",
"TunnelID": "8c559c7c-42bb-4b9d-96a2-99cefd274b06"
}

View File

@@ -0,0 +1,10 @@
services:
tunnel:
build:
context: .
dockerfile: Dockerfile.tunnel
restart: unless-stopped
network_mode: "host"
environment:
# Define cloudflared environment variables
- NO_AUTOUPDATE=true

View File

@@ -9,11 +9,13 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@react-oauth/google": "^0.12.2",
"lucide-react": "^0.483.0", "lucide-react": "^0.483.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-resizable-panels": "^2.1.7" "react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.9.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.21.0",
@@ -1041,6 +1043,16 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
} }
}, },
"node_modules/@react-oauth/google": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz",
"integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.36.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz",
@@ -1602,6 +1614,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2656,9 +2677,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.3", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2676,7 +2697,7 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.8", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.1"
}, },
@@ -2745,6 +2766,44 @@
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
} }
}, },
"node_modules/react-router": {
"version": "7.9.4",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.9.4",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
"integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
"license": "MIT",
"dependencies": {
"react-router": "7.9.4"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2810,6 +2869,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -11,11 +11,13 @@
}, },
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@react-oauth/google": "^0.12.2",
"lucide-react": "^0.483.0", "lucide-react": "^0.483.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-resizable-panels": "^2.1.7" "react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.9.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.21.0", "@eslint/js": "^9.21.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 KiB

BIN
Frontend/public/Bottom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
Frontend/public/kjsce2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,20 +1,50 @@
import CodeChallenge from "./components/CodeChallenge.jsx" import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import "./index.css" import { GoogleOAuthProvider } from '@react-oauth/google';
import { AuthProvider } from './contexts/AuthContext';
import Login from './components/Login';
import TestList from './components/TestList';
import CodeChallenge from "./components/CodeChallenge.jsx";
import Header from './components/Header';
import Footer from './components/Footer';
import ProtectedRoute from './components/ProtectedRoute';
import "./index.css";
function App() { function App() {
// Google OAuth Client ID - in production, this should be in environment variables
const GOOGLE_CLIENT_ID = "586378657128-smg8t52eqbji66c3eg967f70hsr54q5r.apps.googleusercontent.com";
return ( return (
<div className="App"> <GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
<CodeChallenge /> <AuthProvider>
<footer className="footer-bar fixed bottom-0 left-0 right-0 border-t border-slate-200/40 dark:border-gray-800/20 bg-black"> <Router>
<div className="flex items-center justify-center h-7"> <div className="App">
<span className="text-xs text-slate-400 dark:text-gray-400 flex items-center"> <Routes>
Copyright © 2025. Made with <Route path="/login" element={<Login />} />
by Ishika and Arnab. <Route
</span> path="/tests"
</div> element={
</footer> <ProtectedRoute>
</div> <TestList />
) <Footer />
</ProtectedRoute>
}
/>
<Route
path="/editor"
element={
<ProtectedRoute>
<CodeChallenge />
<Footer />
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/tests" replace />} />
</Routes>
</div>
</Router>
</AuthProvider>
</GoogleOAuthProvider>
);
} }
export default App export default App

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
const Footer = () => {
return (
<footer className="footer-bar fixed bottom-0 left-0 right-0 border-t border-slate-200/40 dark:border-gray-800/20 bg-black">
<div className="flex items-center justify-center h-7">
<span className="text-xs text-slate-400 dark:text-gray-400 flex items-center">
Copyright © 2025. Made with
by Ishika and Arnab.
</span>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,41 @@
import { useAuth } from '../contexts/AuthContext';
import { useNavigate } from 'react-router-dom';
const Header = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/login');
};
return (
<header className="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between">
<div className="flex items-center space-x-4">
<h1 className="text-white font-bold text-lg">Monaco Editor</h1>
</div>
<div className="flex items-center space-x-4">
{user?.picture && (
<img
src={user.picture}
alt="Profile"
className="w-8 h-8 rounded-full"
/>
)}
<span className="text-gray-300 text-sm">
Welcome, {user?.name || user?.email}
</span>
<button
onClick={handleLogout}
className="bg-red-600 hover:bg-red-700 text-white text-sm px-3 py-1 rounded transition-colors duration-200"
>
Logout
</button>
</div>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,161 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { GoogleLogin } from '@react-oauth/google';
import { useAuth } from '../contexts/AuthContext';
import ThemeToggle from './ThemeToggle';
const Login = () => {
const [error, setError] = useState('');
const navigate = useNavigate();
const { login } = useAuth();
// Check if user is already logged in
useEffect(() => {
const token = localStorage.getItem('monaco_token');
if (token) {
navigate('/tests');
}
}, [navigate]);
const handleLoginSuccess = async (credentialResponse) => {
console.log('Google login success:', credentialResponse);
if (credentialResponse.credential) {
try {
console.log('Processing Google credential...');
// For demo purposes, we'll decode the JWT token to get user info
// In a real app, you would send this to your backend for verification
const base64Url = credentialResponse.credential.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
const userInfo = JSON.parse(jsonPayload);
console.log('User info:', userInfo);
const success = await login(userInfo.email, credentialResponse.credential, userInfo);
if (success) {
navigate('/tests');
} else {
throw new Error('Authentication failed');
}
} catch (error) {
console.error('Error during login:', error);
setError(`Authentication failed: ${error.message || 'Please try again.'}`);
}
} else {
console.error('No credential received from Google');
setError('No credential received from Google. Please try again.');
}
};
const handleLoginError = () => {
console.error('Google login failed - checking network and configuration...');
// Check if we're online
if (!navigator.onLine) {
setError('You appear to be offline. Please check your internet connection and try again.');
return;
}
// Check if Google Identity Services script loaded
if (typeof window !== 'undefined' && !window.google) {
console.error('Google Identity Services script not loaded');
setError('Google authentication service is not available. Please refresh the page and try again.');
return;
}
setError('Google login failed. This might be due to network connectivity issues, browser compatibility, or Google account configuration. Please try again or contact support if the problem persists.');
};
return (
<div className="login-container">
{/* Theme Toggle - Top Right */}
<div className="theme-toggle">
<ThemeToggle />
</div>
{/* Left Side - Background Image */}
<div className="login-left">
<img
src="/BG-login(2).jpg"
alt="Login Background"
className="login-bg-image"
/>
</div>
{/* Right Side - Login Form */}
<div className="login-right">
<div className="login-form-container">
{/* Logos */}
<div className="login-logos">
<img
src="/Vidyavihar@3x.png"
alt="KJSCE"
className="login-logo"
/>
<img
src="/kjsce2x.png"
alt="Somaiya Vidyavihar"
className="login-logo"
/>
</div>
{/* Title */}
<h1 className="login-title">
Welcome To Monaco Editor
</h1>
<p className="login-subtitle">
Please sign in with your Google account to continue.
</p>
{/* Error Display */}
{error && (
<div className="login-error">
<p style={{ fontSize: '0.875rem', margin: 0 }}>{error}</p>
</div>
)}
{/* Google Login Button */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '1.5rem' }}>
<GoogleLogin
onSuccess={handleLoginSuccess}
onError={handleLoginError}
useOneTap
/>
</div>
<div className="login-footer">
<p className="login-footer-text">
Need help?{' '}
<button className="login-footer-link">
Contact admin
</button>
</p>
</div>
<div className="login-demo-note">
<p className="login-demo-text">
Sign in with your Google account to access the Monaco Editor
</p>
</div>
{/* Trust Logo */}
<div className="login-trust-logo">
<img
src="/Bottom.png"
alt="Somaiya Trust"
className="trust-logo-img"
/>
</div>
</div>
</div>
</div>
);
};
export default Login;

View File

@@ -0,0 +1,21 @@
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
const ProtectedRoute = ({ children }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
<p className="mt-4 text-white">Loading...</p>
</div>
</div>
);
}
return isAuthenticated ? children : <Navigate to="/login" replace />;
};
export default ProtectedRoute;

View File

@@ -0,0 +1,688 @@
/* TestList.css */
.test-list-container {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.dark .test-list-container {
background: linear-gradient(135deg, #1a1a1a 0%, #2d3748 100%);
}
.test-list-header {
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border-bottom: 1px solid #e5e7eb;
padding: 1.5rem 0;
}
.dark .test-list-header {
background: #1f2937;
border-bottom-color: #374151;
}
.header-content {
max-width: 1280px;
margin: 0 auto;
padding: 0 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 768px) {
.header-content {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
}
.header-title {
font-size: 2rem;
font-weight: 700;
color: #111827;
margin: 0 0 0.5rem 0;
}
.dark .header-title {
color: white;
}
.header-subtitle {
color: #6b7280;
margin: 0;
}
.dark .header-subtitle {
color: #9ca3af;
}
.filter-tabs {
display: flex;
gap: 0.5rem;
background: #f3f4f6;
padding: 0.25rem;
border-radius: 0.5rem;
}
.dark .filter-tabs {
background: #374151;
}
.filter-tab {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
border: none;
background: transparent;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
}
.filter-tab:hover {
color: #111827;
}
.dark .filter-tab {
color: #9ca3af;
}
.dark .filter-tab:hover {
color: white;
}
.filter-tab.active {
background: white;
color: #2563eb;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.dark .filter-tab.active {
background: #4b5563;
color: #60a5fa;
}
.test-list-content {
max-width: 1280px;
margin: 0 auto;
padding: 2rem 1rem;
}
.tests-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 768px) {
.tests-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.tests-grid {
grid-template-columns: repeat(3, 1fr);
}
}
.test-card {
background: white;
border-radius: 0.75rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
border: 1px solid #e5e7eb;
transition: all 0.3s;
}
.test-card:hover {
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
border-color: #93c5fd;
transform: translateY(-2px);
}
.dark .test-card {
background: #1f2937;
border-color: #374151;
}
.dark .test-card:hover {
border-color: #3b82f6;
}
.test-card-stripe {
height: 0.5rem;
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
}
.test-card-content {
padding: 1.5rem;
}
.test-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 1rem;
}
.test-title {
font-size: 1.25rem;
font-weight: 700;
color: #111827;
margin: 0;
transition: color 0.2s;
}
.test-card:hover .test-title {
color: #2563eb;
}
.dark .test-title {
color: white;
}
.dark .test-card:hover .test-title {
color: #60a5fa;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
}
.status-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 9999px;
}
.status-active .status-dot {
background: #10b981;
animation: pulse 2s infinite;
}
.status-active {
background: #d1fae5;
color: #065f46;
}
.dark .status-active {
background: rgba(16, 185, 129, 0.2);
color: #6ee7b7;
}
.status-upcoming {
background: #dbeafe;
color: #1e40af;
}
.status-upcoming .status-dot {
background: #3b82f6;
}
.dark .status-upcoming {
background: rgba(59, 130, 246, 0.2);
color: #93c5fd;
}
.status-closed {
background: #f3f4f6;
color: #6b7280;
}
.status-closed .status-dot {
background: #9ca3af;
}
.dark .status-closed {
background: #374151;
color: #9ca3af;
}
.test-description {
color: #6b7280;
margin-bottom: 1.5rem;
min-height: 3rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.dark .test-description {
color: #9ca3af;
}
.test-details {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.test-detail {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: #374151;
}
.dark .test-detail {
color: #d1d5db;
}
.test-detail-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}
.icon-blue {
color: #3b82f6;
}
.icon-purple {
color: #8b5cf6;
}
.icon-amber {
color: #f59e0b;
}
.test-button {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
font-weight: 600;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: all 0.2s;
}
.test-button-active {
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
color: white;
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
}
.test-button-active:hover {
transform: translateY(-1px);
box-shadow: 0 6px 8px rgba(59, 130, 246, 0.4);
}
.test-button-disabled {
background: #e5e7eb;
color: #9ca3af;
cursor: not-allowed;
}
.dark .test-button-disabled {
background: #374151;
color: #6b7280;
}
.loading-container,
.error-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.dark .loading-container,
.dark .error-container {
background: linear-gradient(135deg, #1a1a1a 0%, #2d3748 100%);
}
.loading-content {
text-align: center;
}
.spinner {
display: inline-block;
width: 3rem;
height: 3rem;
border: 0.25rem solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
.loading-text {
color: #6b7280;
font-size: 1.125rem;
}
.dark .loading-text {
color: #9ca3af;
}
.error-box {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 0.5rem;
padding: 1.5rem;
max-width: 28rem;
}
.dark .error-box {
background: rgba(220, 38, 38, 0.1);
border-color: #991b1b;
}
.error-content {
display: flex;
align-items: center;
gap: 0.75rem;
}
.error-icon {
width: 1.5rem;
height: 1.5rem;
color: #dc2626;
flex-shrink: 0;
}
.error-title {
font-weight: 600;
color: #991b1b;
margin: 0 0 0.25rem 0;
}
.dark .error-title {
color: #fca5a5;
}
.error-message {
color: #dc2626;
margin: 0;
}
.dark .error-message {
color: #fca5a5;
}
.empty-state {
text-align: center;
padding: 4rem 1rem;
}
.empty-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
border-radius: 50%;
background: #e5e7eb;
margin-bottom: 1rem;
}
.dark .empty-icon {
background: #374151;
}
.empty-icon svg {
width: 2rem;
height: 2rem;
color: #9ca3af;
}
.empty-title {
font-size: 1.125rem;
font-weight: 600;
color: #111827;
margin: 0 0 0.5rem 0;
}
.dark .empty-title {
color: white;
}
.empty-message {
color: #6b7280;
margin: 0;
}
.dark .empty-message {
color: #9ca3af;
}
/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 1000;
animation: fadeIn 0.3s;
}
.modal-content {
background: white;
border-radius: 1rem;
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 28rem;
animation: slideUp 0.3s;
}
.dark .modal-content {
background: #1f2937;
}
.modal-header {
border-bottom: 1px solid #e5e7eb;
padding: 1.5rem;
}
.dark .modal-header {
border-bottom-color: #374151;
}
.modal-header-content {
display: flex;
align-items: center;
gap: 0.75rem;
}
.modal-icon {
width: 3rem;
height: 3rem;
border-radius: 50%;
background: #dbeafe;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.dark .modal-icon {
background: rgba(59, 130, 246, 0.2);
}
.modal-icon svg {
width: 1.5rem;
height: 1.5rem;
color: #3b82f6;
}
.dark .modal-icon svg {
color: #60a5fa;
}
.modal-title {
font-size: 1.25rem;
font-weight: 700;
color: #111827;
margin: 0;
}
.dark .modal-title {
color: white;
}
.modal-subtitle {
font-size: 0.875rem;
color: #6b7280;
margin: 0;
}
.dark .modal-subtitle {
color: #9ca3af;
}
.modal-body {
padding: 1.5rem;
}
.modal-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.5rem;
}
.dark .modal-label {
color: #d1d5db;
}
.modal-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 1rem;
transition: all 0.2s;
box-sizing: border-box;
}
.modal-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.dark .modal-input {
background: #374151;
border-color: #4b5563;
color: white;
}
.dark .modal-input:focus {
border-color: #3b82f6;
}
.modal-footer {
border-top: 1px solid #e5e7eb;
padding: 1.5rem;
display: flex;
gap: 0.75rem;
}
.dark .modal-footer {
border-top-color: #374151;
}
.modal-button {
flex: 1;
padding: 0.625rem 1rem;
border-radius: 0.5rem;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.modal-button-cancel {
background: #f3f4f6;
color: #374151;
}
.modal-button-cancel:hover {
background: #e5e7eb;
}
.dark .modal-button-cancel {
background: #374151;
color: #d1d5db;
}
.dark .modal-button-cancel:hover {
background: #4b5563;
}
.modal-button-submit {
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
color: white;
}
.modal-button-submit:hover:not(:disabled) {
opacity: 0.9;
}
.modal-button-submit:disabled {
background: #9ca3af;
cursor: not-allowed;
}
.dark .modal-button-submit:disabled {
background: #4b5563;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(1.25rem);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

View File

@@ -0,0 +1,354 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import './TestList.css';
const TestList = () => {
const [tests, setTests] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [selectedTest, setSelectedTest] = useState(null);
const [password, setPassword] = useState('');
const [filterStatus, setFilterStatus] = useState('all');
const navigate = useNavigate();
const { token } = useAuth();
useEffect(() => {
fetchTests();
}, []);
const fetchTests = async () => {
try {
console.log('Fetching tests with token:', token?.substring(0, 50) + '...');
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
const response = await fetch(`${API_URL}/students/tests`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
if (response.status === 401) {
localStorage.removeItem('monaco_user');
localStorage.removeItem('monaco_token');
window.location.href = '/login';
return;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
console.log('Tests received:', data.tests);
data.tests.forEach(test => {
console.log(`Test: ${test.title}, Status: ${test.status}, Start: ${test.start_time}, End: ${test.end_time}`);
});
setTests(data.tests);
} else {
setError(data.message || 'Failed to fetch tests');
}
} catch (error) {
setError('Failed to fetch tests');
console.error('Error:', error);
} finally {
setLoading(false);
}
};
const handleStartTest = async (test) => {
try {
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
const response = await fetch(`${API_URL}/students/tests/${test.id}/questions`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await response.json();
if (data.success) {
localStorage.setItem('currentTest', JSON.stringify({
id: test.id,
title: test.title,
description: test.description,
duration_minutes: test.duration_minutes,
start_time: test.start_time,
end_time: test.end_time,
questions: data.questions,
currentQuestionIndex: 0
}));
navigate('/editor');
} else {
setError(data.message);
}
} catch (error) {
setError('Failed to start test');
console.error('Error:', error);
}
};
const handlePasswordSubmit = async () => {
if (!selectedTest || !password) return;
try {
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
const response = await fetch(`${API_URL}/students/tests/${selectedTest.id}/verify-password`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ password })
});
const data = await response.json();
if (data.success) {
setShowPasswordModal(false);
setPassword('');
handleStartTest(selectedTest);
} else {
setError('Invalid password');
}
} catch (error) {
setError('Failed to verify password');
console.error('Error:', error);
}
};
const handleTestClick = (test) => {
if (test.password_required) {
setSelectedTest(test);
setShowPasswordModal(true);
} else {
handleStartTest(test);
}
};
const filteredTests = tests.filter(test => {
if (filterStatus === 'all') return true;
return test.status.toLowerCase() === filterStatus.toLowerCase();
});
if (loading) {
return (
<div className="loading-container">
<div className="loading-content">
<div className="spinner"></div>
<p className="loading-text">Loading tests...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="error-container">
<div className="error-box">
<div className="error-content">
<svg className="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 className="error-title">Error</h3>
<p className="error-message">{error}</p>
</div>
</div>
</div>
</div>
);
}
return (
<div className="test-list-container">
{/* Header Section */}
<div className="test-list-header">
<div className="header-content">
<div>
<h1 className="header-title">
📝 Available Tests
</h1>
<p className="header-subtitle">
Select a test to start your coding challenge
</p>
</div>
{/* Filter Tabs */}
<div className="filter-tabs">
<button
onClick={() => setFilterStatus('all')}
className={`filter-tab ${filterStatus === 'all' ? 'active' : ''}`}
>
All Tests
</button>
<button
onClick={() => setFilterStatus('active')}
className={`filter-tab ${filterStatus === 'active' ? 'active' : ''}`}
>
Active
</button>
<button
onClick={() => setFilterStatus('upcoming')}
className={`filter-tab ${filterStatus === 'upcoming' ? 'active' : ''}`}
>
Upcoming
</button>
</div>
</div>
</div>
{/* Tests Grid */}
<div className="test-list-content">
{filteredTests.length === 0 ? (
<div className="empty-state">
<div className="empty-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="empty-title">No tests available</h3>
<p className="empty-message">Check back later for new tests</p>
</div>
) : (
<div className="tests-grid">
{filteredTests.map(test => (
<div key={test.id} className="test-card">
{/* Status Badge */}
<div className="test-card-stripe"></div>
<div className="test-card-content">
{/* Header */}
<div className="test-card-header">
<h2 className="test-title">
{test.title}
</h2>
<span className={`status-badge status-${test.status?.toLowerCase() || 'closed'}`}>
<span className="status-dot"></span>
{test.status}
</span>
</div>
{/* Description */}
<p className="test-description">
{test.description || 'No description available'}
</p>
{/* Test Details */}
<div className="test-details">
<div className="test-detail">
<svg className="test-detail-icon icon-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span><strong>{test.duration_minutes}</strong> minutes</span>
</div>
{test.total_questions && (
<div className="test-detail">
<svg className="test-detail-icon icon-purple" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span><strong>{test.total_questions}</strong> questions</span>
</div>
)}
{test.password_required && (
<div className="test-detail icon-amber">
<svg className="test-detail-icon icon-amber" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span><strong>Password required</strong></span>
</div>
)}
</div>
{/* Action Button */}
<button
onClick={() => handleTestClick(test)}
disabled={test.status !== 'Active'}
className={`test-button ${
test.status === 'Active' ? 'test-button-active' : 'test-button-disabled'
}`}
>
{test.status === 'Active' ? (
<>
<span>Start Test</span>
<svg style={{width: '1.25rem', height: '1.25rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</>
) : (
<>
<svg style={{width: '1.25rem', height: '1.25rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span>Not Available</span>
</>
)}
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Password Modal */}
{showPasswordModal && (
<div className="modal-overlay">
<div className="modal-content">
{/* Modal Header */}
<div className="modal-header">
<div className="modal-header-content">
<div className="modal-icon">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<div>
<h2 className="modal-title">Protected Test</h2>
<p className="modal-subtitle">Enter password to continue</p>
</div>
</div>
</div>
{/* Modal Body */}
<div className="modal-body">
<label className="modal-label">
Test Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handlePasswordSubmit()}
className="modal-input"
placeholder="Enter password"
autoFocus
/>
</div>
{/* Modal Footer */}
<div className="modal-footer">
<button
onClick={() => {
setShowPasswordModal(false);
setPassword('');
setError(null);
}}
className="modal-button modal-button-cancel"
>
Cancel
</button>
<button
onClick={handlePasswordSubmit}
disabled={!password}
className="modal-button modal-button-submit"
>
Submit
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default TestList;

View File

@@ -0,0 +1,61 @@
import { useState, useEffect } from 'react';
const ThemeToggle = () => {
const [theme, setTheme] = useState('dark');
useEffect(() => {
// Check if there's a saved theme preference
const savedTheme = localStorage.getItem('monaco_theme');
if (savedTheme) {
setTheme(savedTheme);
} else {
// Check system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(prefersDark ? 'dark' : 'light');
}
}, []);
useEffect(() => {
// Update the HTML class and CSS variables
const root = document.documentElement;
root.className = theme;
// Save theme preference
localStorage.setItem('monaco_theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<button
onClick={toggleTheme}
className="relative p-2 rounded-md border border-gray-600 bg-gray-800 hover:bg-gray-700 transition-colors"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
>
{/* Sun Icon for Light Mode */}
<svg
className={`h-5 w-5 transition-all ${theme === 'dark' ? 'rotate-90 scale-0' : 'rotate-0 scale-100'}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<circle cx="12" cy="12" r="5" />
<path d="M12 1v2m0 18v2M4.22 4.22l1.42 1.42m12.72 12.72l1.42 1.42M1 12h2m18 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
</svg>
{/* Moon Icon for Dark Mode */}
<svg
className={`absolute top-2 left-2 h-5 w-5 transition-all ${theme === 'dark' ? 'rotate-0 scale-100' : 'rotate-90 scale-0'}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
</svg>
</button>
);
};
export default ThemeToggle;

View File

@@ -0,0 +1,121 @@
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [token, setToken] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Check for existing session on app load
useEffect(() => {
const checkAuthStatus = () => {
try {
const savedUser = localStorage.getItem('monaco_user');
const savedToken = localStorage.getItem('monaco_token');
if (savedUser && savedToken) {
setUser(JSON.parse(savedUser));
setToken(savedToken);
}
} catch (error) {
console.error('Error checking auth status:', error);
localStorage.removeItem('monaco_user');
localStorage.removeItem('monaco_token');
} finally {
setIsLoading(false);
}
};
checkAuthStatus();
}, []);
const login = async (email, googleToken, userInfo = null) => {
// For Google OAuth login
if (googleToken && userInfo) {
// Exchange Google token for our JWT
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
const response = await fetch(`${API_URL}/students/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${googleToken}`
},
body: JSON.stringify({
email: email,
name: userInfo.name || email.split('@')[0]
})
});
if (!response.ok) {
throw new Error(`Server error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Login failed');
}
const userData = {
id: userInfo.sub || Date.now(),
email: email,
name: userInfo.name || email.split('@')[0],
picture: userInfo.picture || null,
loginTime: new Date().toISOString(),
token: data.token // Store the JWT instead of Google token
};
setUser(userData);
localStorage.setItem('monaco_user', JSON.stringify(userData));
localStorage.setItem('monaco_token', userData.token);
setToken(userData.token);
return true;
}
// Fallback for demo purposes (though we're moving to Google-only)
if (email && email.includes('@')) {
const userData = {
id: Date.now(),
email: email,
name: email.split('@')[0],
loginTime: new Date().toISOString()
};
setUser(userData);
localStorage.setItem('monaco_user', JSON.stringify(userData));
localStorage.setItem('monaco_token', 'demo_token');
return true;
}
return false;
};
const logout = () => {
setUser(null);
setToken(null);
localStorage.removeItem('monaco_user');
localStorage.removeItem('monaco_token');
};
const value = {
user,
token,
login,
logout,
isAuthenticated: !!user,
isLoading
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
export const studentApi = {
async getTests() {
const response = await fetch(`${API_URL}/students/tests`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('monaco_token')}`
}
});
return await response.json();
},
async getTestQuestions(testId) {
const response = await fetch(`${API_URL}/students/tests/${testId}/questions`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('monaco_token')}`
}
});
return await response.json();
},
async verifyTestPassword(testId, password) {
const response = await fetch(`${API_URL}/students/tests/${testId}/verify-password`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('monaco_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ password })
});
return await response.json();
},
async submitAnswer(testId, questionId, code) {
const response = await fetch(`${API_URL}/students/submissions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('monaco_token')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
testId,
answers: [{
questionId,
submittedAnswer: code
}]
})
});
return await response.json();
}
};

51
Frontend/start-tunnel.sh Normal file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${GREEN}================================${NC}"
echo -e "${GREEN}Monaco Frontend Tunnel Setup${NC}"
echo -e "${GREEN}================================${NC}"
echo ""
# Check if credentials.json exists
if [ ! -f "credentials.json" ]; then
echo -e "${RED}Error: credentials.json not found!${NC}"
exit 1
fi
# Check if config.json exists
if [ ! -f "config.json" ]; then
echo -e "${RED}Error: config.json not found!${NC}"
exit 1
fi
echo -e "${YELLOW}Building Docker image...${NC}"
docker-compose -f docker-compose.tunnel.yml build
if [ $? -eq 0 ]; then
echo -e "${GREEN}Build successful!${NC}"
echo ""
echo -e "${YELLOW}Starting services...${NC}"
docker-compose -f docker-compose.tunnel.yml up -d
if [ $? -eq 0 ]; then
echo -e "${GREEN}Services started successfully!${NC}"
echo ""
echo -e "${GREEN}Frontend is now accessible at:${NC}"
echo -e " ${YELLOW}Local:${NC} http://localhost:8001"
echo -e " ${YELLOW}Tunnel:${NC} https://monaco.ishikabhoyar.tech"
echo ""
echo -e "${YELLOW}To view logs:${NC} docker-compose -f docker-compose.tunnel.yml logs -f"
echo -e "${YELLOW}To stop:${NC} docker-compose -f docker-compose.tunnel.yml down"
else
echo -e "${RED}Failed to start services!${NC}"
exit 1
fi
else
echo -e "${RED}Build failed!${NC}"
exit 1
fi

View File

@@ -1,5 +1,6 @@
# Monaco Code Execution Engine # Monaco Code Execution Engine
Test 1
Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API and WebSocket connections for real-time terminal interaction. Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API and WebSocket connections for real-time terminal interaction.
## Features ## Features
@@ -57,6 +58,7 @@ Monaco consists of several components:
```bash ```bash
git clone https://github.com/arnab-afk/monaco.git git clone https://github.com/arnab-afk/monaco.git
cd monaco/backend cd monaco/backend
```
2.Install Go dependencies: 2.Install Go dependencies:
@@ -65,11 +67,13 @@ Monaco consists of several components:
``` ```
3.Build the application: 3.Build the application:
```bash ```bash
go build -o monaco go build -o monaco
``` ```
4.Run the service 4.Run the service
```bash ```bash
./monaco ./monaco
``` ```
@@ -77,22 +81,27 @@ go build -o monaco
The backend service will start on port 8080 by default. The backend service will start on port 8080 by default.
### Frontend Setup ### Frontend Setup
1. Navigate to the Frontend directory: 1. Navigate to the Frontend directory:
```bash ```bash
cd Frontend cd Frontend
``` ```
2. Install dependencies: 2. Install dependencies:
```bash ```bash
npm install npm install
``` ```
3. Set up environment variables: Create a ```.env``` or ```.env.local.``` file with: 3. Set up environment variables: Create a `.env` or `.env.local.` file with:
```bash ```bash
VITE_API_URL=http://localhost:8080 VITE_API_URL=http://localhost:8080
``` ```
4. Start the development server: 4. Start the development server:
```bash ```bash
npm run dev npm run dev
``` ```
@@ -102,9 +111,11 @@ The frontend will be available at http://localhost:5173 by default.
### API Reference ### API Reference
### REST Endpoints ### REST Endpoints
```POST /submit```
`POST /submit`
Submits code for execution Submits code for execution
```json ```json
{ {
"language": "python", "language": "python",
@@ -114,15 +125,17 @@ Submits code for execution
``` ```
Response: Response:
```json ```json
{ {
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1" "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1"
} }
``` ```
```GET /status?id={submissionId}``` `GET /status?id={submissionId}`
Checks the status of submission: Checks the status of submission:
```json ```json
{ {
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1", "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
@@ -134,11 +147,12 @@ Checks the status of submission:
} }
``` ```
```GET /result?id={submissionId}``` `GET /result?id={submissionId}`
Gets the execution result of a submission. Gets the execution result of a submission.
Response: Response:
```json ```json
{ {
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1", "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
@@ -155,10 +169,11 @@ Response:
} }
``` ```
```GET /queue-stats``` `GET /queue-stats`
Gets the statistics about the job queue. Gets the statistics about the job queue.
Response: Response:
```json ```json
{ {
"queue_stats": { "queue_stats": {
@@ -171,7 +186,8 @@ Response:
``` ```
### WebSocket Endpoints ### WebSocket Endpoints
```ws://localhost:8080/ws/terminal?id={submissionId}```
`ws://localhost:8080/ws/terminal?id={submissionId}`
Establishes a real-time connection for terminal interaction. Establishes a real-time connection for terminal interaction.
@@ -180,18 +196,18 @@ Establishes a real-time connection for terminal interaction.
- Connection automatically closes when execution completes or fails. - Connection automatically closes when execution completes or fails.
### Terminal Input Handling ### Terminal Input Handling
The system supports interactive programs requiring user input: The system supports interactive programs requiring user input:
1. The frontend detects possible input prompts by looking for patterns 1. The frontend detects possible input prompts by looking for patterns
2. When detected, it focuses the terminal and allows user input 2. When detected, it focuses the terminal and allows user input
3. User input is captured in the terminal component's inputBuffer 3. User input is captured in the terminal component's inputBuffer
4. When the user presses Enter, the input is: 4. When the user presses Enter, the input is:
- Sent to the backend via WebSocket. - Sent to the backend via WebSocket.
- Displayed in the terminal. - Displayed in the terminal.
- Buffer is cleared for next input. - Buffer is cleared for next input.
5. The input is processed by the running program in real-time. 5. The input is processed by the running program in real-time.
Troubleshooting tips: Troubleshooting tips:
- Ensure WebSocket connection is established before sending input - Ensure WebSocket connection is established before sending input
@@ -200,41 +216,49 @@ Troubleshooting tips:
- Ensure newline characters are properly appended to input. - Ensure newline characters are properly appended to input.
### Language Support ### Language Support
### Python ### Python
- **Version**: Python 3.9 - **Version**: Python 3.9
- **Input Handling**: Direct stdin piping - **Input Handling**: Direct stdin piping
- **Limitations**: No file I/O, no package imports outside standard library - **Limitations**: No file I/O, no package imports outside standard library
- **Resource Limits**: 100MB memory, 10% CPU - **Resource Limits**: 100MB memory, 10% CPU
### Java ### Java
- **Version**: Java 11 (Eclipse Temurin) - **Version**: Java 11 (Eclipse Temurin)
- **Class Detection**: Extracts class name from code using regex. - **Class Detection**: Extracts class name from code using regex.
- **Memory Settings**: 64MB min heap, 256MB max heap - **Memory Settings**: 64MB min heap, 256MB max heap
- **Resource Limits**: 400MB memory, 50% CPU - **Resource Limits**: 400MB memory, 50% CPU
C C
- **Version**: Latest GCC - **Version**: Latest GCC
- **Compilation Flags**: Default GCC settings - **Compilation Flags**: Default GCC settings
- **Resource Limits**: 100MB memory, 10% CPU - **Resource Limits**: 100MB memory, 10% CPU
### C++ ### C++
- **Version**: Latest G++ - **Version**: Latest G++
- **Standard**: C++17 - **Standard**: C++17
- **Resource Limits**: 100MB memory, 10% CPU - **Resource Limits**: 100MB memory, 10% CPU
### Security Considerations ### Security Considerations
All code execution happens within isolated Docker containers with: All code execution happens within isolated Docker containers with:
- No network access (```--network=none```) - No network access (`--network=none`)
- Limited CPU and memory resources - Limited CPU and memory resources
- Limited file system access - Limited file system access
- No persistent storage - No persistent storage
- Execution time limits (10-15 seconds) - Execution time limits (10-15 seconds)
### Debugging ### Debugging
Check backend logs for execution details Check backend logs for execution details
Use browser developer tools to debug WebSocket connections Use browser developer tools to debug WebSocket connections
Terminal panel shows WebSocket connection status and errors Terminal panel shows WebSocket connection status and errors
Check Docker logs for container-related issues. Check Docker logs for container-related issues.
### Contributing ### Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Contributions are welcome! Please feel free to submit a Pull Request.
Contributions are welcome! Please feel free to submit a Pull Request :).

147
UI_MODERNIZATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,147 @@
# UI Modernization Summary
## Overview
Successfully modernized the entire UI to match a clean, professional exam interface design while preserving all existing functionalities.
## Key Changes
### 🎨 Code Challenge Page (Editor Interface)
#### 1. **Modern Header**
- Added left section with emoji icon (📝) and exam title
- Created sophisticated timer display with:
- Time Remaining label
- Separate blocks for Hours : Minutes : Seconds
- Color-coded with soft red background (#fee2e2)
- Labels below timer blocks
- Added user profile avatar with gradient background
- Clean white background with subtle shadow
#### 2. **Question Palette (Left Sidebar)**
- Transformed from vertical tabs to a modern grid layout
- Added "Question Palette" header
- 4-column grid of question buttons (20 questions)
- Current question highlighted in blue (#2563eb)
- Added legend at bottom:
- Current (blue dot)
- Answered (green dot)
- Not Visited (gray dot)
- Clean spacing and rounded corners
#### 3. **Problem Content Area**
- Added question header showing "Question X of Y | 10 Points"
- Modern test case cards with:
- Gray header labels (Example 1, Example 2)
- Bordered code blocks for input/output
- Clean, readable typography
- Added action buttons at bottom:
- "Clear Response" with reset icon
- "Mark for Review" with checkmark icon
- Better spacing and visual hierarchy
#### 4. **Editor Section**
- Modernized language selector dropdown
- Updated Run and Submit buttons:
- Run: Green (#10b981) with play icon
- Submit: Blue (#2563eb) with send icon
- Smooth hover effects with shadows
- Better padding and spacing
#### 5. **Terminal/Console Section**
- Added tab navigation (Console / Testcases)
- Active tab highlighted in blue
- Empty state placeholder: "Console output will appear here..."
- Modern terminal prompt with white background box
- Improved readability with better colors
#### 6. **Footer Action Bar**
- New fixed footer with:
- "Run Code" button (outline style)
- "Save & Next" button (primary blue)
- "Submit Test" button (success green)
- "All changes saved" indicator with checkmark
- Professional shadow and spacing
### 🎯 Test List Page
Already had modern styling with:
- Gradient backgrounds
- Card-based layout
- Status badges
- Smooth animations
- Clean modals
## Color Scheme
### Primary Colors
- **Blue**: #2563eb (Primary actions, active states)
- **Green**: #10b981 (Success, Run button)
- **Red**: #dc2626 (Timer, errors)
### Neutral Colors
- **Background**: #ffffff (White)
- **Secondary BG**: #f9fafb (Light gray)
- **Borders**: #e5e7eb (Light gray borders)
- **Text Primary**: #111827 (Dark gray)
- **Text Secondary**: #6b7280 (Medium gray)
- **Text Muted**: #9ca3af (Light gray)
### Accent Colors
- Timer blocks: #fee2e2 (Light red)
- User avatar: Linear gradient (#667eea to #764ba2)
- Status badges: Various semantic colors
## Typography
- **Font Family**: System fonts (-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto)
- **Code Font**: 'Consolas', 'Monaco', monospace
- **Font Sizes**:
- Headers: 18-20px
- Body: 14px
- Small text: 12-13px
- **Font Weights**: 400 (normal), 500 (medium), 600 (semi-bold), 700 (bold)
## Spacing & Layout
- Consistent padding: 12px, 16px, 24px
- Gap spacing: 6px, 8px, 12px
- Border radius: 6px, 8px
- Box shadows for depth
## Interactive Elements
- Smooth transitions (0.2s ease)
- Hover states with background changes
- Active states clearly distinguished
- Disabled states with reduced opacity
- Button hover effects with subtle shadows
## Responsive Behavior
- Flexbox layouts for adaptability
- Grid systems for question palette
- Scrollable content areas
- Fixed header and footer
## Accessibility Features
- Proper color contrast
- Clear visual feedback
- Semantic HTML structure
- Keyboard navigable elements
- Focus states preserved
## All Functionalities Preserved
✅ Timer countdown
✅ Question navigation
✅ Code execution
✅ WebSocket terminal communication
✅ Code submission
✅ Language selection
✅ Test case display
✅ User authentication flow
✅ Test list filtering
✅ Password-protected tests
✅ Auto-save submissions
## Files Modified
1. `/Frontend/src/index.css` - Complete UI styling overhaul
2. `/Frontend/src/components/CodeChallenge.jsx` - Updated JSX structure for new components
3. All existing functionality remains intact
## Result
A clean, modern, and professional examination interface that matches industry standards while maintaining all existing features and functionality.

View File

@@ -0,0 +1,24 @@
# Tunnel-only Dockerfile - Backend runs outside Docker on port 9090
FROM alpine:latest
# Install wget to download cloudflared
RUN apk update && apk add --no-cache wget ca-certificates
# Get cloudflared directly from GitHub
RUN wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/download/2023.5.0/cloudflared-linux-amd64 && \
chmod +x cloudflared && \
mv cloudflared /usr/local/bin/
# Create directories for cloudflared
RUN mkdir -p /etc/cloudflared
# Copy the certificate file and config
COPY cert.pem /etc/cloudflared/cert.pem
COPY credentials.json /etc/cloudflared/credentials.json
COPY config.tunnel-only.json /etc/cloudflared/config.json
# Setup DNS routing for the tunnel (only needs to be done once)
RUN cloudflared tunnel route dns 5d2682ef-0b5b-47e5-b0fa-ad48968ce016 api.ishikabhoyar.tech || echo "DNS routing already set up or failed - continuing anyway"
# Run cloudflared tunnel
CMD ["cloudflared", "tunnel", "--config", "/etc/cloudflared/config.json", "run"]

View File

@@ -0,0 +1,24 @@
# Tunnel-only Dockerfile V2 - Uses localhost with host network mode
FROM alpine:latest
# Install wget to download cloudflared
RUN apk update && apk add --no-cache wget ca-certificates
# Get cloudflared directly from GitHub
RUN wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/download/2023.5.0/cloudflared-linux-amd64 && \
chmod +x cloudflared && \
mv cloudflared /usr/local/bin/
# Create directories for cloudflared
RUN mkdir -p /etc/cloudflared
# Copy the certificate file and config
COPY cert.pem /etc/cloudflared/cert.pem
COPY credentials.json /etc/cloudflared/credentials.json
COPY config.tunnel-only-v2.json /etc/cloudflared/config.json
# Setup DNS routing for the tunnel (only needs to be done once)
RUN cloudflared tunnel route dns 5d2682ef-0b5b-47e5-b0fa-ad48968ce016 api.ishikabhoyar.tech || echo "DNS routing already set up or failed - continuing anyway"
# Run cloudflared tunnel
CMD ["cloudflared", "tunnel", "--config", "/etc/cloudflared/config.json", "run"]

View File

@@ -0,0 +1,115 @@
# Monaco Backend - Tunnel Only Setup
This setup runs **only the Cloudflare tunnel** in Docker, while the backend runs **outside Docker on port 9090**.
## Prerequisites
1. Backend must be running on port 9090 on your local machine
2. Required files in this directory:
- `cert.pem` - Cloudflare tunnel certificate
- `credentials.json` - Cloudflare tunnel credentials
- `config.tunnel-only.json` - Tunnel configuration (points to port 9090)
## Setup
### 1. Start your backend on port 9090
Run your Go backend locally:
```bash
# Option 1: Run directly
PORT=9090 go run main.go
# Option 2: Build and run
go build -o main
PORT=9090 ./main
```
### 2. Start the tunnel
In this directory, run:
```bash
docker-compose -f docker-compose.tunnel-only.yml up --build
```
Or run in detached mode:
```bash
docker-compose -f docker-compose.tunnel-only.yml up --build -d
```
### 3. Check logs
```bash
docker-compose -f docker-compose.tunnel-only.yml logs -f
```
## How It Works
1. The tunnel container runs only `cloudflared`
2. It connects to Cloudflare's edge network
3. Traffic from `api.ishikabhoyar.tech` is routed through the tunnel
4. The tunnel forwards requests to `host.docker.internal:9090` (your local backend)
5. Your backend on port 9090 handles the requests and sends responses back
## Configuration
The tunnel is configured in `config.tunnel-only.json`:
```json
{
"tunnel": "5d2682ef-0b5b-47e5-b0fa-ad48968ce016",
"credentials-file": "/etc/cloudflared/credentials.json",
"ingress": [
{
"hostname": "api.ishikabhoyar.tech",
"service": "http://host.docker.internal:9090"
},
{
"service": "http_status:404"
}
],
"protocol": "http2",
"loglevel": "info"
}
```
## Troubleshooting
### Tunnel can't reach backend
- Make sure your backend is running on port 9090
- Test locally: `curl http://localhost:9090`
- Check firewall settings
### Tunnel connection issues
- Verify `credentials.json` and `cert.pem` are valid
- Check tunnel status in Cloudflare dashboard
- Review logs: `docker-compose -f docker-compose.tunnel-only.yml logs -f`
### DNS not resolving
- DNS routing should be set up during first build
- Verify in Cloudflare dashboard under Zero Trust > Networks > Tunnels
## Stop the tunnel
```bash
docker-compose -f docker-compose.tunnel-only.yml down
```
## Architecture
```
Internet
Cloudflare Edge (api.ishikabhoyar.tech)
Cloudflare Tunnel (in Docker)
host.docker.internal:9090
Your Backend (running locally)
```
## Notes
- The tunnel only forwards traffic; it doesn't run the backend
- Backend must be started before or after the tunnel (order doesn't matter)
- If backend restarts, tunnel will automatically reconnect
- Port 9090 is not exposed to the internet, only accessible via the tunnel

View File

@@ -0,0 +1,123 @@
# Tunnel Connection Options - Troubleshooting Guide
## The Problem
When the tunnel runs in Docker and tries to reach your backend on the host, there are different ways to address the host machine. The method that works depends on your Docker setup.
## Solution 1: Direct Bridge IP (config.tunnel-only.json) ✅ WORKING
**Files:**
- `Dockerfile.tunnel-only`
- `docker-compose.tunnel-only.yml`
- `config.tunnel-only.json` (updated to use `172.18.0.1:9090`)
**How it works:**
- Uses the Docker bridge network IP directly
- You verified this works: `curl 172.18.0.1:9090`
**Usage:**
```bash
docker-compose -f docker-compose.tunnel-only.yml up --build
```
**Config:**
```json
"service": "http://172.18.0.1:9090"
```
## Solution 2: Host Network Mode (NEW - Recommended for Linux)
**Files:**
- `Dockerfile.tunnel-only-v2`
- `docker-compose.tunnel-only-v2.yml`
- `config.tunnel-only-v2.json` (uses `localhost:9090`)
**How it works:**
- Container runs in host network mode
- Can access `localhost:9090` directly as if running on host
**Usage:**
```bash
docker-compose -f docker-compose.tunnel-only-v2.yml up --build
```
**Config:**
```json
"service": "http://localhost:9090"
```
**Note:** Host network mode works best on Linux. May have limitations on Windows/Mac.
## Quick Test Guide
### 1. Rebuild and restart with updated config (Solution 1)
```bash
# Stop current tunnel
docker-compose -f docker-compose.tunnel-only.yml down
# Rebuild with updated config (now uses 172.18.0.1)
docker-compose -f docker-compose.tunnel-only.yml up --build
```
### 2. Or try host network mode (Solution 2)
```bash
docker-compose -f docker-compose.tunnel-only-v2.yml up --build
```
## Expected Success Output
```
INF Registered tunnel connection connIndex=0 connection=xxx event=0 ip=xxx location=bom protocol=http2
INF Registered tunnel connection connIndex=1 connection=xxx event=0 ip=xxx location=bom protocol=http2
INF Registered tunnel connection connIndex=2 connection=xxx event=0 ip=xxx location=bom protocol=http2
INF Registered tunnel connection connIndex=3 connection=xxx event=0 ip=xxx location=bom protocol=http2
```
**No "Unable to reach the origin service" errors!**
## Test the Connection
### From outside Docker (your current working test):
```bash
curl 172.18.0.1:9090
# Should return: Monaco Code Execution Server v1.0.0
```
### From the tunnel (once running):
```bash
# Test via the public URL
curl https://api.ishikabhoyar.tech
```
## Troubleshooting
### If Solution 1 still doesn't work:
1. Check if Docker bridge IP changed:
```bash
docker network inspect bridge | grep Gateway
```
2. Update `config.tunnel-only.json` with the correct IP
### If Solution 2 doesn't work:
- Host network mode may not be fully supported on your OS
- Fall back to Solution 1 with correct bridge IP
### Check tunnel logs:
```bash
docker-compose -f docker-compose.tunnel-only.yml logs -f
```
### Verify backend is accessible from Docker:
```bash
docker run --rm alpine/curl:latest curl http://172.18.0.1:9090
```
## Summary
**Current Status:** ✅ Config updated to use `172.18.0.1:9090`
**Next Step:** Rebuild and restart the tunnel:
```bash
docker-compose -f docker-compose.tunnel-only.yml down
docker-compose -f docker-compose.tunnel-only.yml up --build
```
The tunnel should now successfully connect to your backend! 🎉

View File

@@ -0,0 +1,69 @@
# Quick Start - Tunnel Only Mode
## What This Does
- Runs **only** the Cloudflare tunnel in Docker
- Your backend runs **outside Docker** on port 9090
- Tunnel forwards traffic from `api.ishikabhoyar.tech` to your local backend
## Quick Start
### Step 1: Start your backend on port 9090
```bash
PORT=9090 go run main.go
```
### Step 2: Start the tunnel
**Windows (PowerShell):**
```powershell
.\start-tunnel-only.ps1
```
**Linux/Mac:**
```bash
chmod +x start-tunnel-only.sh
./start-tunnel-only.sh
```
**Or manually:**
```bash
docker-compose -f docker-compose.tunnel-only.yml up --build
```
## Files Created
1. **Dockerfile.tunnel-only** - Lightweight Docker image with only cloudflared
2. **docker-compose.tunnel-only.yml** - Docker Compose config for tunnel only
3. **config.tunnel-only.json** - Cloudflare tunnel config pointing to port 9090
4. **start-tunnel-only.ps1** - PowerShell helper script
5. **start-tunnel-only.sh** - Bash helper script
6. **README.tunnel-only.md** - Detailed documentation
## Test It
1. Start backend: `PORT=9090 go run main.go`
2. Start tunnel: `docker-compose -f docker-compose.tunnel-only.yml up --build`
3. Test: `curl https://api.ishikabhoyar.tech`
## Stop
```bash
docker-compose -f docker-compose.tunnel-only.yml down
```
## Troubleshooting
**Backend not reachable?**
- Check backend is running: `curl http://localhost:9090`
- Check tunnel logs: `docker-compose -f docker-compose.tunnel-only.yml logs -f`
**Tunnel not connecting?**
- Verify credentials.json and cert.pem are valid
- Check Cloudflare dashboard
## Original Files (Unchanged)
The original tunnel setup files are still available:
- `Dockerfile.tunnel` - Backend + Tunnel in one container
- `docker-compose.tunnel.yml` - Original compose file
- These files still point to port 8080

View File

@@ -4,7 +4,7 @@
"ingress": [ "ingress": [
{ {
"hostname": "api.ishikabhoyar.tech", "hostname": "api.ishikabhoyar.tech",
"service": "http://localhost:8080" "service": "http://host.docker.internal:8080"
}, },
{ {
"service": "http_status:404" "service": "http_status:404"

View File

@@ -0,0 +1,15 @@
{
"tunnel": "5d2682ef-0b5b-47e5-b0fa-ad48968ce016",
"credentials-file": "/etc/cloudflared/credentials.json",
"ingress": [
{
"hostname": "api.ishikabhoyar.tech",
"service": "http://localhost:9090"
},
{
"service": "http_status:404"
}
],
"protocol": "http2",
"loglevel": "info"
}

View File

@@ -0,0 +1,15 @@
{
"tunnel": "5d2682ef-0b5b-47e5-b0fa-ad48968ce016",
"credentials-file": "/etc/cloudflared/credentials.json",
"ingress": [
{
"hostname": "api.ishikabhoyar.tech",
"service": "http://172.18.0.1:9090"
},
{
"service": "http_status:404"
}
],
"protocol": "http2",
"loglevel": "info"
}

View File

@@ -0,0 +1,11 @@
services:
tunnel:
build:
context: .
dockerfile: Dockerfile.tunnel-only
restart: unless-stopped
# Use host network mode to directly access localhost:9090
network_mode: "host"
environment:
- TUNNEL_ORIGIN_CERT=/etc/cloudflared/cert.pem
- NO_AUTOUPDATE=true

View File

@@ -0,0 +1,18 @@
services:
tunnel:
build:
context: .
dockerfile: Dockerfile.tunnel-only
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- TUNNEL_ORIGIN_CERT=/etc/cloudflared/cert.pem
- NO_AUTOUPDATE=true
# Isolated network
networks:
- monaco-tunnel-network
networks:
monaco-tunnel-network:
driver: bridge

View File

@@ -4,11 +4,13 @@ services:
context: . context: .
dockerfile: Dockerfile.tunnel dockerfile: Dockerfile.tunnel
restart: unless-stopped restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
volumes: volumes:
- //var/run/docker.sock:/var/run/docker.sock - //var/run/docker.sock:/var/run/docker.sock
# Port is only exposed locally, traffic comes through the tunnel # No need to expose ports - traffic comes through the tunnel
ports: # ports:
- "127.0.0.1:8080:8080" # - "127.0.0.1:8080:8080"
environment: environment:
- PORT=8080 - PORT=8080
- CONCURRENT_EXECUTIONS=5 - CONCURRENT_EXECUTIONS=5

View File

@@ -21,13 +21,26 @@ import (
"github.com/ishikabhoyar/monaco/new-backend/models" "github.com/ishikabhoyar/monaco/new-backend/models"
) )
// SafeWebSocketConn wraps a WebSocket connection with a mutex for thread-safe writes
type SafeWebSocketConn struct {
conn *websocket.Conn
mutex sync.Mutex
}
// WriteJSON writes JSON to the WebSocket connection in a thread-safe manner
func (s *SafeWebSocketConn) WriteJSON(v interface{}) error {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.conn.WriteJSON(v)
}
// CodeExecutor handles code execution for all languages // CodeExecutor handles code execution for all languages
type CodeExecutor struct { type CodeExecutor struct {
config *config.Config config *config.Config
execQueue chan *models.CodeSubmission execQueue chan *models.CodeSubmission
submissions map[string]*models.CodeSubmission submissions map[string]*models.CodeSubmission
submissionsMutex sync.RWMutex submissionsMutex sync.RWMutex
terminalConnections map[string][]*websocket.Conn terminalConnections map[string][]*SafeWebSocketConn
terminalMutex sync.RWMutex terminalMutex sync.RWMutex
inputChannels map[string]chan string inputChannels map[string]chan string
inputMutex sync.RWMutex inputMutex sync.RWMutex
@@ -39,7 +52,7 @@ func NewCodeExecutor(cfg *config.Config) *CodeExecutor {
config: cfg, config: cfg,
execQueue: make(chan *models.CodeSubmission, cfg.Executor.QueueCapacity), execQueue: make(chan *models.CodeSubmission, cfg.Executor.QueueCapacity),
submissions: make(map[string]*models.CodeSubmission), submissions: make(map[string]*models.CodeSubmission),
terminalConnections: make(map[string][]*websocket.Conn), terminalConnections: make(map[string][]*SafeWebSocketConn),
inputChannels: make(map[string]chan string), inputChannels: make(map[string]chan string),
} }
@@ -87,7 +100,8 @@ func (e *CodeExecutor) RegisterTerminalConnection(submissionID string, conn *web
e.terminalMutex.Lock() e.terminalMutex.Lock()
defer e.terminalMutex.Unlock() defer e.terminalMutex.Unlock()
e.terminalConnections[submissionID] = append(e.terminalConnections[submissionID], conn) safeConn := &SafeWebSocketConn{conn: conn}
e.terminalConnections[submissionID] = append(e.terminalConnections[submissionID], safeConn)
log.Printf("WebSocket connection registered for submission %s (total: %d)", log.Printf("WebSocket connection registered for submission %s (total: %d)",
submissionID, len(e.terminalConnections[submissionID])) submissionID, len(e.terminalConnections[submissionID]))
@@ -103,13 +117,14 @@ func (e *CodeExecutor) UnregisterTerminalConnection(submissionID string, conn *w
connections := e.terminalConnections[submissionID] connections := e.terminalConnections[submissionID]
for i, c := range connections { for i, c := range connections {
if c == conn { if c.conn == conn {
// Remove the connection // Remove the connection
e.terminalConnections[submissionID] = append(connections[:i], connections[i+1:]...) e.terminalConnections[submissionID] = append(connections[:i], connections[i+1:]...)
break break
} }
} }
// Clean up if no more connections // Clean up if no more connections
if len(e.terminalConnections[submissionID]) == 0 { if len(e.terminalConnections[submissionID]) == 0 {
delete(e.terminalConnections, submissionID) delete(e.terminalConnections, submissionID)
@@ -326,7 +341,7 @@ func (e *CodeExecutor) executeJava(submission *models.CodeSubmission, tempDir st
// executeC executes C code // executeC executes C code
func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) { func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file // Write code to file exactly as submitted by user
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt) codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil { if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed" submission.Status = "failed"
@@ -334,62 +349,6 @@ func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir strin
return return
} }
// Create a wrapper script that will include setbuf to disable buffering
wrapperCode := `#include <stdio.h>
// Forward declaration of user's main function
int user_main();
int main() {
// Disable buffering completely for stdout
setbuf(stdout, NULL);
// Call the user's code
return user_main();
}
// User's code begins here
`
// Modify the user's code to be a function called from our wrapper
modifiedCode := submission.Code
// Replace main function with our wrapper
mainRegex := regexp.MustCompile(`int\s+main\s*\([^)]*\)\s*{`)
if mainRegex.MatchString(modifiedCode) {
// Rename user's main to user_main
modifiedCode = mainRegex.ReplaceAllString(modifiedCode, "int user_main() {")
// Combine wrapper with modified user code
finalCode := wrapperCode + modifiedCode
// Write the final code with wrapper to file
if err := os.WriteFile(codeFile, []byte(finalCode), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
} else {
// If no main function found, create a minimal program that includes the user code
finalCode := `#include <stdio.h>
int main() {
// Disable buffering completely for stdout
setbuf(stdout, NULL);
// Execute the user's code
` + submission.Code + `
return 0;
}
`
// Write the final code to file
if err := os.WriteFile(codeFile, []byte(finalCode), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
}
// Compile C code // Compile C code
compileCmd := exec.Command( compileCmd := exec.Command(
"docker", "run", "--rm", "docker", "run", "--rm",
@@ -406,7 +365,7 @@ int main() {
return return
} }
// Setup Docker run command // Setup Docker run command with stdbuf to force line buffering
cmd := exec.Command( cmd := exec.Command(
"docker", "run", "--rm", "-i", "docker", "run", "--rm", "-i",
"--network=none", "--network=none",
@@ -415,7 +374,7 @@ int main() {
"--pids-limit=20", "--pids-limit=20",
"-v", tempDir+":/code", "-v", tempDir+":/code",
langConfig.Image, langConfig.Image,
"/code/program", "stdbuf", "-oL", "-eL", "/code/program",
) )
// Execute the code with input handling // Execute the code with input handling
@@ -448,7 +407,7 @@ func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir str
return return
} }
// Setup Docker run command // Setup Docker run command with stdbuf to force line buffering
cmd := exec.Command( cmd := exec.Command(
"docker", "run", "--rm", "-i", "docker", "run", "--rm", "-i",
"--network=none", "--network=none",
@@ -457,7 +416,7 @@ func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir str
"--pids-limit=20", "--pids-limit=20",
"-v", tempDir+":/code", "-v", tempDir+":/code",
langConfig.Image, langConfig.Image,
"/code/program", "stdbuf", "-oL", "-eL", "/code/program",
) )
// Execute the code with input handling // Execute the code with input handling

Binary file not shown.

BIN
new-backend/new-backend Executable file

Binary file not shown.

View File

@@ -0,0 +1,42 @@
# Monaco Backend - Tunnel Only Runner
# This script helps you run the tunnel with backend on port 9090
Write-Host "Monaco Backend - Tunnel Only Setup" -ForegroundColor Green
Write-Host "====================================`n" -ForegroundColor Green
# Check if required files exist
$requiredFiles = @("cert.pem", "credentials.json", "config.tunnel-only.json")
$missingFiles = @()
foreach ($file in $requiredFiles) {
if (-not (Test-Path $file)) {
$missingFiles += $file
}
}
if ($missingFiles.Count -gt 0) {
Write-Host "ERROR: Missing required files:" -ForegroundColor Red
foreach ($file in $missingFiles) {
Write-Host " - $file" -ForegroundColor Red
}
Write-Host "`nPlease ensure all required files are present." -ForegroundColor Yellow
exit 1
}
Write-Host "✓ All required files found`n" -ForegroundColor Green
# Check if backend is running on port 9090
Write-Host "Checking if backend is running on port 9090..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "http://localhost:9090" -TimeoutSec 2 -ErrorAction Stop
Write-Host "✓ Backend is running on port 9090`n" -ForegroundColor Green
} catch {
Write-Host "⚠ Backend doesn't appear to be running on port 9090" -ForegroundColor Yellow
Write-Host " Make sure to start your backend with: PORT=9090 go run main.go`n" -ForegroundColor Yellow
}
# Start the tunnel
Write-Host "Starting Cloudflare tunnel..." -ForegroundColor Cyan
Write-Host "Command: docker-compose -f docker-compose.tunnel-only.yml up --build`n" -ForegroundColor Gray
docker-compose -f docker-compose.tunnel-only.yml up --build

View File

@@ -0,0 +1,48 @@
#!/bin/bash
# Monaco Backend - Tunnel Only Runner
# This script helps you run the tunnel with backend on port 9090
echo "Monaco Backend - Tunnel Only Setup"
echo "===================================="
echo ""
# Check if required files exist
REQUIRED_FILES=("cert.pem" "credentials.json" "config.tunnel-only.json")
MISSING_FILES=()
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$file" ]; then
MISSING_FILES+=("$file")
fi
done
if [ ${#MISSING_FILES[@]} -ne 0 ]; then
echo "ERROR: Missing required files:"
for file in "${MISSING_FILES[@]}"; do
echo " - $file"
done
echo ""
echo "Please ensure all required files are present."
exit 1
fi
echo "✓ All required files found"
echo ""
# Check if backend is running on port 9090
echo "Checking if backend is running on port 9090..."
if curl -s -o /dev/null -w "%{http_code}" http://localhost:9090 > /dev/null 2>&1; then
echo "✓ Backend is running on port 9090"
else
echo "⚠ Backend doesn't appear to be running on port 9090"
echo " Make sure to start your backend with: PORT=9090 go run main.go"
fi
echo ""
# Start the tunnel
echo "Starting Cloudflare tunnel..."
echo "Command: docker-compose -f docker-compose.tunnel-only.yml up --build"
echo ""
docker-compose -f docker-compose.tunnel-only.yml up --build