Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e35d086b0 | |||
| 917de3e401 | |||
| 2e089e192d | |||
| 38cf325e22 | |||
| fff316d5ec | |||
| 16620111cf | |||
|
|
fbbcabbeab | ||
|
|
35d40da727 | ||
| ebaef13845 | |||
| b075e5b3d3 | |||
|
|
1d3b1c74e1 | ||
|
|
8ec0935486 | ||
|
|
33b9e2fe38 | ||
|
|
c579f972b8 | ||
|
|
0c844c3122 | ||
| 356d532beb | |||
|
|
2508731ec7 | ||
| 6ef2edb768 | |||
| fbafab5d51 | |||
| a433c5067f | |||
|
|
ec33604a6f | ||
|
|
dbd8770f20 | ||
|
|
9fa0528ff0 | ||
|
|
e82554215b | ||
|
|
abc15efabd | ||
|
|
b18dc5f21b | ||
|
|
336ad17240 | ||
|
|
9d180e13b1 | ||
|
|
6b98938245 | ||
|
|
9a1dee00a1 | ||
|
|
9d6729e63c | ||
|
|
4daafa726d | ||
| 47f73681af | |||
|
|
304761e258 | ||
|
|
453f44a43a | ||
|
|
e8e6011524 |
@@ -1 +1,2 @@
|
||||
VITE_API_URL="http://localhost:8080"
|
||||
VITE_FACULTY_API_URL="http://localhost:5000/api"
|
||||
7
Frontend/.env.example
Normal file
7
Frontend/.env.example
Normal 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
|
||||
35
Frontend/Dockerfile.tunnel
Normal file
35
Frontend/Dockerfile.tunnel
Normal 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
15
Frontend/config.json
Normal 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"
|
||||
}
|
||||
5
Frontend/credentials.json
Normal file
5
Frontend/credentials.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"AccountTag": "453afb9373a00a55876e6127cf0efd97",
|
||||
"TunnelSecret": "afJ6YY25rj9+G6qqHy+2jss4h+QKfw6YntijRZvo4ZQ=",
|
||||
"TunnelID": "8c559c7c-42bb-4b9d-96a2-99cefd274b06"
|
||||
}
|
||||
10
Frontend/docker-compose.tunnel.yml
Normal file
10
Frontend/docker-compose.tunnel.yml
Normal 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
|
||||
75
Frontend/package-lock.json
generated
75
Frontend/package-lock.json
generated
@@ -9,11 +9,13 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-oauth/google": "^0.12.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"react": "^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": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
@@ -1041,6 +1043,16 @@
|
||||
"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": {
|
||||
"version": "4.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz",
|
||||
@@ -1602,6 +1614,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2656,9 +2677,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2676,7 +2697,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -2810,6 +2869,12 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-oauth/google": "^0.12.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"react": "^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": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
|
||||
BIN
Frontend/public/BG-login(2).jpg
Normal file
BIN
Frontend/public/BG-login(2).jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 677 KiB |
BIN
Frontend/public/Bottom.png
Normal file
BIN
Frontend/public/Bottom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Frontend/public/Vidyavihar@3x.png
Normal file
BIN
Frontend/public/Vidyavihar@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
Frontend/public/kjsce2x.png
Normal file
BIN
Frontend/public/kjsce2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1,20 +1,50 @@
|
||||
import CodeChallenge from "./components/CodeChallenge.jsx"
|
||||
import "./index.css"
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
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() {
|
||||
// Google OAuth Client ID - in production, this should be in environment variables
|
||||
const GOOGLE_CLIENT_ID = "586378657128-smg8t52eqbji66c3eg967f70hsr54q5r.apps.googleusercontent.com";
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<CodeChallenge />
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/tests"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
14
Frontend/src/components/Footer.jsx
Normal file
14
Frontend/src/components/Footer.jsx
Normal 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;
|
||||
41
Frontend/src/components/Header.jsx
Normal file
41
Frontend/src/components/Header.jsx
Normal 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;
|
||||
161
Frontend/src/components/Login.jsx
Normal file
161
Frontend/src/components/Login.jsx
Normal 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;
|
||||
21
Frontend/src/components/ProtectedRoute.jsx
Normal file
21
Frontend/src/components/ProtectedRoute.jsx
Normal 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;
|
||||
688
Frontend/src/components/TestList.css
Normal file
688
Frontend/src/components/TestList.css
Normal 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;
|
||||
}
|
||||
}
|
||||
354
Frontend/src/components/TestList.jsx
Normal file
354
Frontend/src/components/TestList.jsx
Normal 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;
|
||||
61
Frontend/src/components/ThemeToggle.jsx
Normal file
61
Frontend/src/components/ThemeToggle.jsx
Normal 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;
|
||||
121
Frontend/src/contexts/AuthContext.jsx
Normal file
121
Frontend/src/contexts/AuthContext.jsx
Normal 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
51
Frontend/src/utils/studentApi.js
Normal file
51
Frontend/src/utils/studentApi.js
Normal 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
51
Frontend/start-tunnel.sh
Normal 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
|
||||
50
Readme.md
50
Readme.md
@@ -1,5 +1,6 @@
|
||||
# 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.
|
||||
|
||||
## Features
|
||||
@@ -57,6 +58,7 @@ Monaco consists of several components:
|
||||
```bash
|
||||
git clone https://github.com/arnab-afk/monaco.git
|
||||
cd monaco/backend
|
||||
```
|
||||
|
||||
2.Install Go dependencies:
|
||||
|
||||
@@ -65,11 +67,13 @@ Monaco consists of several components:
|
||||
```
|
||||
|
||||
3.Build the application:
|
||||
|
||||
```bash
|
||||
go build -o monaco
|
||||
```
|
||||
|
||||
4.Run the service
|
||||
|
||||
```bash
|
||||
./monaco
|
||||
```
|
||||
@@ -77,22 +81,27 @@ go build -o monaco
|
||||
The backend service will start on port 8080 by default.
|
||||
|
||||
### Frontend Setup
|
||||
|
||||
1. Navigate to the Frontend directory:
|
||||
|
||||
```bash
|
||||
cd Frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
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
|
||||
VITE_API_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
4. Start the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
@@ -102,9 +111,11 @@ The frontend will be available at http://localhost:5173 by default.
|
||||
### API Reference
|
||||
|
||||
### REST Endpoints
|
||||
```POST /submit```
|
||||
|
||||
`POST /submit`
|
||||
|
||||
Submits code for execution
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "python",
|
||||
@@ -114,15 +125,17 @@ Submits code for execution
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1"
|
||||
}
|
||||
```
|
||||
|
||||
```GET /status?id={submissionId}```
|
||||
`GET /status?id={submissionId}`
|
||||
|
||||
Checks the status of submission:
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
|
||||
@@ -155,10 +169,11 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
```GET /queue-stats```
|
||||
`GET /queue-stats`
|
||||
Gets the statistics about the job queue.
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"queue_stats": {
|
||||
@@ -171,7 +186,8 @@ Response:
|
||||
```
|
||||
|
||||
### WebSocket Endpoints
|
||||
```ws://localhost:8080/ws/terminal?id={submissionId}```
|
||||
|
||||
`ws://localhost:8080/ws/terminal?id={submissionId}`
|
||||
|
||||
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.
|
||||
|
||||
### Terminal Input Handling
|
||||
|
||||
The system supports interactive programs requiring user input:
|
||||
|
||||
1. The frontend detects possible input prompts by looking for patterns
|
||||
2. When detected, it focuses the terminal and allows user input
|
||||
3. User input is captured in the terminal component's inputBuffer
|
||||
4. When the user presses Enter, the input is:
|
||||
- Sent to the backend via WebSocket.
|
||||
- Displayed in the terminal.
|
||||
- Buffer is cleared for next input.
|
||||
- Sent to the backend via WebSocket.
|
||||
- Displayed in the terminal.
|
||||
- Buffer is cleared for next input.
|
||||
5. The input is processed by the running program in real-time.
|
||||
|
||||
|
||||
Troubleshooting tips:
|
||||
|
||||
- Ensure WebSocket connection is established before sending input
|
||||
@@ -200,41 +216,49 @@ Troubleshooting tips:
|
||||
- Ensure newline characters are properly appended to input.
|
||||
|
||||
### Language Support
|
||||
|
||||
### Python
|
||||
|
||||
- **Version**: Python 3.9
|
||||
- **Input Handling**: Direct stdin piping
|
||||
- **Limitations**: No file I/O, no package imports outside standard library
|
||||
- **Resource Limits**: 100MB memory, 10% CPU
|
||||
|
||||
### Java
|
||||
|
||||
- **Version**: Java 11 (Eclipse Temurin)
|
||||
- **Class Detection**: Extracts class name from code using regex.
|
||||
- **Memory Settings**: 64MB min heap, 256MB max heap
|
||||
- **Resource Limits**: 400MB memory, 50% CPU
|
||||
C
|
||||
C
|
||||
- **Version**: Latest GCC
|
||||
- **Compilation Flags**: Default GCC settings
|
||||
- **Resource Limits**: 100MB memory, 10% CPU
|
||||
|
||||
### C++
|
||||
|
||||
- **Version**: Latest G++
|
||||
- **Standard**: C++17
|
||||
- **Resource Limits**: 100MB memory, 10% CPU
|
||||
|
||||
### Security Considerations
|
||||
|
||||
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 file system access
|
||||
- No persistent storage
|
||||
- Execution time limits (10-15 seconds)
|
||||
|
||||
### Debugging
|
||||
|
||||
Check backend logs for execution details
|
||||
Use browser developer tools to debug WebSocket connections
|
||||
Terminal panel shows WebSocket connection status and errors
|
||||
Check Docker logs for container-related issues.
|
||||
|
||||
### 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
147
UI_MODERNIZATION_SUMMARY.md
Normal 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.
|
||||
24
new-backend/Dockerfile.tunnel-only
Normal file
24
new-backend/Dockerfile.tunnel-only
Normal 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"]
|
||||
24
new-backend/Dockerfile.tunnel-only-v2
Normal file
24
new-backend/Dockerfile.tunnel-only-v2
Normal 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"]
|
||||
115
new-backend/README.tunnel-only.md
Normal file
115
new-backend/README.tunnel-only.md
Normal 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
|
||||
123
new-backend/TUNNEL-CONNECTION-FIX.md
Normal file
123
new-backend/TUNNEL-CONNECTION-FIX.md
Normal 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! 🎉
|
||||
69
new-backend/TUNNEL-ONLY-QUICKSTART.md
Normal file
69
new-backend/TUNNEL-ONLY-QUICKSTART.md
Normal 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
|
||||
@@ -4,7 +4,7 @@
|
||||
"ingress": [
|
||||
{
|
||||
"hostname": "api.ishikabhoyar.tech",
|
||||
"service": "http://localhost:8080"
|
||||
"service": "http://host.docker.internal:8080"
|
||||
},
|
||||
{
|
||||
"service": "http_status:404"
|
||||
|
||||
15
new-backend/config.tunnel-only-v2.json
Normal file
15
new-backend/config.tunnel-only-v2.json
Normal 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"
|
||||
}
|
||||
15
new-backend/config.tunnel-only.json
Normal file
15
new-backend/config.tunnel-only.json
Normal 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"
|
||||
}
|
||||
11
new-backend/docker-compose.tunnel-only-v2.yml
Normal file
11
new-backend/docker-compose.tunnel-only-v2.yml
Normal 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
|
||||
18
new-backend/docker-compose.tunnel-only.yml
Normal file
18
new-backend/docker-compose.tunnel-only.yml
Normal 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
|
||||
@@ -4,11 +4,13 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile.tunnel
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
volumes:
|
||||
- //var/run/docker.sock:/var/run/docker.sock
|
||||
# Port is only exposed locally, traffic comes through the tunnel
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
# No need to expose ports - traffic comes through the tunnel
|
||||
# ports:
|
||||
# - "127.0.0.1:8080:8080"
|
||||
environment:
|
||||
- PORT=8080
|
||||
- CONCURRENT_EXECUTIONS=5
|
||||
|
||||
@@ -21,13 +21,26 @@ import (
|
||||
"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
|
||||
type CodeExecutor struct {
|
||||
config *config.Config
|
||||
execQueue chan *models.CodeSubmission
|
||||
submissions map[string]*models.CodeSubmission
|
||||
submissionsMutex sync.RWMutex
|
||||
terminalConnections map[string][]*websocket.Conn
|
||||
terminalConnections map[string][]*SafeWebSocketConn
|
||||
terminalMutex sync.RWMutex
|
||||
inputChannels map[string]chan string
|
||||
inputMutex sync.RWMutex
|
||||
@@ -39,7 +52,7 @@ func NewCodeExecutor(cfg *config.Config) *CodeExecutor {
|
||||
config: cfg,
|
||||
execQueue: make(chan *models.CodeSubmission, cfg.Executor.QueueCapacity),
|
||||
submissions: make(map[string]*models.CodeSubmission),
|
||||
terminalConnections: make(map[string][]*websocket.Conn),
|
||||
terminalConnections: make(map[string][]*SafeWebSocketConn),
|
||||
inputChannels: make(map[string]chan string),
|
||||
}
|
||||
|
||||
@@ -87,7 +100,8 @@ func (e *CodeExecutor) RegisterTerminalConnection(submissionID string, conn *web
|
||||
e.terminalMutex.Lock()
|
||||
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)",
|
||||
submissionID, len(e.terminalConnections[submissionID]))
|
||||
@@ -103,13 +117,14 @@ func (e *CodeExecutor) UnregisterTerminalConnection(submissionID string, conn *w
|
||||
|
||||
connections := e.terminalConnections[submissionID]
|
||||
for i, c := range connections {
|
||||
if c == conn {
|
||||
if c.conn == conn {
|
||||
// Remove the connection
|
||||
e.terminalConnections[submissionID] = append(connections[:i], connections[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clean up if no more connections
|
||||
if len(e.terminalConnections[submissionID]) == 0 {
|
||||
delete(e.terminalConnections, submissionID)
|
||||
@@ -326,7 +341,7 @@ func (e *CodeExecutor) executeJava(submission *models.CodeSubmission, tempDir st
|
||||
|
||||
// executeC executes C code
|
||||
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)
|
||||
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
@@ -334,62 +349,6 @@ func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir strin
|
||||
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
|
||||
compileCmd := exec.Command(
|
||||
"docker", "run", "--rm",
|
||||
@@ -406,7 +365,7 @@ int main() {
|
||||
return
|
||||
}
|
||||
|
||||
// Setup Docker run command
|
||||
// Setup Docker run command with stdbuf to force line buffering
|
||||
cmd := exec.Command(
|
||||
"docker", "run", "--rm", "-i",
|
||||
"--network=none",
|
||||
@@ -415,7 +374,7 @@ int main() {
|
||||
"--pids-limit=20",
|
||||
"-v", tempDir+":/code",
|
||||
langConfig.Image,
|
||||
"/code/program",
|
||||
"stdbuf", "-oL", "-eL", "/code/program",
|
||||
)
|
||||
|
||||
// Execute the code with input handling
|
||||
@@ -448,7 +407,7 @@ func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir str
|
||||
return
|
||||
}
|
||||
|
||||
// Setup Docker run command
|
||||
// Setup Docker run command with stdbuf to force line buffering
|
||||
cmd := exec.Command(
|
||||
"docker", "run", "--rm", "-i",
|
||||
"--network=none",
|
||||
@@ -457,7 +416,7 @@ func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir str
|
||||
"--pids-limit=20",
|
||||
"-v", tempDir+":/code",
|
||||
langConfig.Image,
|
||||
"/code/program",
|
||||
"stdbuf", "-oL", "-eL", "/code/program",
|
||||
)
|
||||
|
||||
// Execute the code with input handling
|
||||
|
||||
BIN
new-backend/main
BIN
new-backend/main
Binary file not shown.
BIN
new-backend/new-backend
Executable file
BIN
new-backend/new-backend
Executable file
Binary file not shown.
42
new-backend/start-tunnel-only.ps1
Normal file
42
new-backend/start-tunnel-only.ps1
Normal 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
|
||||
48
new-backend/start-tunnel-only.sh
Normal file
48
new-backend/start-tunnel-only.sh
Normal 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
|
||||
Reference in New Issue
Block a user