293 lines
11 KiB
JavaScript
293 lines
11 KiB
JavaScript
|
const http = require('http');
|
||
|
const fs = require('fs');
|
||
|
const path = require('path');
|
||
|
const ejs = require('ejs');
|
||
|
const os = require('os');
|
||
|
const QRCode = require('qrcode');
|
||
|
const { handleApiRequest } = require('./routes/api');
|
||
|
const { validateSession, authorize, acl } = require('./acl');
|
||
|
const WebSocket = require('ws');
|
||
|
const cookie = require('cookie');
|
||
|
const Session = require('./models/session');
|
||
|
const { v4: uuidv4 } = require('uuid');
|
||
|
const nms = require('./media-server');
|
||
|
const httpProxy = require('http-proxy');
|
||
|
|
||
|
const port = process.env.PORT || 3000;
|
||
|
|
||
|
// Proxy setup
|
||
|
const proxy = httpProxy.createServer({
|
||
|
target: 'http://localhost:8000/',
|
||
|
changeOrigin: true
|
||
|
});
|
||
|
|
||
|
const connections = {}; // Store connections with confirmation codes
|
||
|
|
||
|
// Function to get the network address
|
||
|
const getNetworkAddress = () => {
|
||
|
if(process.env['DEVELOPMENT']) {
|
||
|
const interfaces = os.networkInterfaces();
|
||
|
for (const name of Object.keys(interfaces)) {
|
||
|
for (const iface of interfaces[name]) {
|
||
|
if (iface.family === 'IPv4' && !iface.internal) {
|
||
|
return iface.address;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return 'localhost';
|
||
|
} else {
|
||
|
return 'the.mk';
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Logging function
|
||
|
const logRequest = (req, res, startTime) => {
|
||
|
const duration = new Date() - startTime;
|
||
|
const logMessage = `${req.method} ${req.url} ${res.statusCode} - ${duration}ms`;
|
||
|
console.log(logMessage);
|
||
|
};
|
||
|
|
||
|
const renderPage = (res, template, data) => {
|
||
|
const filePath = path.join(__dirname, 'views', 'layouts', 'master.ejs');
|
||
|
ejs.renderFile(filePath, { ...data, template }, (err, str) => {
|
||
|
if (err) {
|
||
|
console.error(err); // Log the error for debugging
|
||
|
if (!res.headersSent) {
|
||
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||
|
}
|
||
|
res.end('Internal Server Error');
|
||
|
} else {
|
||
|
if (!res.headersSent) {
|
||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||
|
}
|
||
|
res.end(str);
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const serveStaticFile = (req, res) => {
|
||
|
const filePath = path.join(__dirname, 'public', req.url);
|
||
|
fs.readFile(filePath, (err, data) => {
|
||
|
if (err) {
|
||
|
if (!res.headersSent) {
|
||
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||
|
}
|
||
|
res.end('Not Found');
|
||
|
} else {
|
||
|
const ext = path.extname(filePath);
|
||
|
const contentType = ext === '.css' ? 'text/css' : 'application/javascript';
|
||
|
if (!res.headersSent) {
|
||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||
|
}
|
||
|
res.end(data);
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const parseCookies = (request) => {
|
||
|
const list = {};
|
||
|
const cookieHeader = request.headers.cookie;
|
||
|
|
||
|
if (cookieHeader) {
|
||
|
cookieHeader.split(';').forEach((cookie) => {
|
||
|
let [name, ...rest] = cookie.split('=');
|
||
|
name = name.trim();
|
||
|
if (!name) return;
|
||
|
const value = rest.join('=').trim();
|
||
|
if (!value) return;
|
||
|
list[name] = decodeURIComponent(value);
|
||
|
});
|
||
|
}
|
||
|
return list;
|
||
|
};
|
||
|
|
||
|
const server = http.createServer(async (req, res) => {
|
||
|
const startTime = new Date();
|
||
|
const cookies = parseCookies(req);
|
||
|
const token = cookies.token;
|
||
|
|
||
|
res.on('finish', () => logRequest(req, res, startTime));
|
||
|
|
||
|
const session = await validateSession(token);
|
||
|
const user = session ? session.data.user : null;
|
||
|
|
||
|
console.log('Session:', session); // Debug: Print session data
|
||
|
console.log('User:', user); // Debug: Print user data
|
||
|
|
||
|
if (req.url.startsWith('/media')) {
|
||
|
// Modify req.url to remove /media before proxying
|
||
|
req.url = req.url.replace(/^\/media/, '');
|
||
|
// Proxy media requests to Node-Media-Server
|
||
|
proxy.web(req, res);
|
||
|
} else if (req.method === 'GET') {
|
||
|
if (req.url.startsWith('/css/') || req.url.startsWith('/js/') || req.url.startsWith('/videos/') || req.url.startsWith('/audio/')) {
|
||
|
serveStaticFile(req, res);
|
||
|
} else {
|
||
|
const permissions = acl[req.url];
|
||
|
const template = permissions ? permissions.template : null;
|
||
|
const authorized = permissions ? authorize(req.url, 'GET', user ? user.role : null) : false;
|
||
|
|
||
|
console.log('Permissions:', permissions); // Debug: Print permissions
|
||
|
console.log('Authorized:', authorized); // Debug: Print authorization status
|
||
|
|
||
|
if (req.url === '/') {
|
||
|
const networkAddress = getNetworkAddress();
|
||
|
const url = `https://${networkAddress}`;
|
||
|
const confirmationCode = uuidv4().split('-')[0]; // Simple confirmation code
|
||
|
connections[confirmationCode] = { desktop: null, mobile: null };
|
||
|
let qrCodeDataUrl = '';
|
||
|
|
||
|
try {
|
||
|
qrCodeDataUrl = await QRCode.toDataURL(`${url}/mobile?code=${confirmationCode}`);
|
||
|
} catch (error) {
|
||
|
console.error('Error generating QR code:', error);
|
||
|
}
|
||
|
|
||
|
renderPage(res, 'index.ejs', { title: 'Welcome to The Learning App', session: session || {}, qrCodeDataUrl, url, confirmationCode });
|
||
|
} else if (req.url.startsWith('/mobile')) {
|
||
|
const code = new URL(req.url, `http://${req.headers.host}`).searchParams.get('code');
|
||
|
if (code && connections[code]) {
|
||
|
renderPage(res, 'mobile.ejs', { title: 'Mobile View', session: session || {}, confirmationCode: code });
|
||
|
} else {
|
||
|
renderPage(res, 'not_found.ejs', { title: 'Not Found', session: session || {} });
|
||
|
}
|
||
|
} else if (template && authorized) {
|
||
|
let sessions = [];
|
||
|
if (req.url === '/sessions') {
|
||
|
sessions = await Session.findAll();
|
||
|
}
|
||
|
renderPage(res, template, { title: req.url.slice(1).toUpperCase(), session: session || {}, sessions });
|
||
|
} else if (authorized === false) {
|
||
|
renderPage(res, 'unauthorized.ejs', { title: 'Unauthorized', session: session || {} });
|
||
|
} else {
|
||
|
renderPage(res, 'not_found.ejs', { title: 'Not Found', session: session || {} });
|
||
|
}
|
||
|
}
|
||
|
} else if (req.method === 'POST') {
|
||
|
if (req.url.startsWith('/api/')) {
|
||
|
handleApiRequest(req, res);
|
||
|
} else {
|
||
|
if (!res.headersSent) {
|
||
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||
|
}
|
||
|
res.end('Not Found');
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const wss = new WebSocket.Server({ noServer: true });
|
||
|
|
||
|
server.on('upgrade', async (request, socket, head) => {
|
||
|
const cookies = parseCookies(request);
|
||
|
const token = cookies.token;
|
||
|
const session = await validateSession(token);
|
||
|
if (!session) {
|
||
|
socket.destroy();
|
||
|
} else {
|
||
|
wss.handleUpgrade(request, socket, head, ws => {
|
||
|
wss.emit('connection', ws, request, session);
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
wss.on('connection', (ws, request, session) => {
|
||
|
ws.on('message', message => {
|
||
|
const msg = JSON.parse(message);
|
||
|
if (msg.type === 'confirm') {
|
||
|
const code = msg.code;
|
||
|
if (connections[code]) {
|
||
|
connections[code].mobile = ws;
|
||
|
ws.send(JSON.stringify({ type: 'confirmation', token: code, content: getCurrentLesson() }));
|
||
|
if (connections[code].desktop) {
|
||
|
connections[code].desktop.send(JSON.stringify({ type: 'confirm', content: getCurrentLesson() }));
|
||
|
connections[code].desktop.send(JSON.stringify({ type: 'welcome' }));
|
||
|
connections[code].mobile.send(JSON.stringify({ type: 'welcome' }));
|
||
|
}
|
||
|
}
|
||
|
} else if (msg.type === 'quizAnswer' || msg.type === 'choice') {
|
||
|
const token = msg.token;
|
||
|
if (connections[token]) {
|
||
|
connections[token].desktop.send(JSON.stringify(msg));
|
||
|
// Automatically proceed to the next lesson
|
||
|
const nextLessonContent = nextLesson();
|
||
|
connections[token].desktop.send(JSON.stringify({ type: 'nextLesson', content: nextLessonContent }));
|
||
|
connections[token].mobile.send(JSON.stringify({ type: 'nextLesson', content: nextLessonContent }));
|
||
|
}
|
||
|
} else if (msg.type === 'videoEnded') {
|
||
|
const token = msg.token;
|
||
|
if (connections[token]) {
|
||
|
connections[token].mobile.send(JSON.stringify({ type: 'showControls' }));
|
||
|
}
|
||
|
} else if (msg.type === 'nextLesson') {
|
||
|
const token = msg.token;
|
||
|
if (connections[token]) {
|
||
|
const nextLessonContent = nextLesson();
|
||
|
connections[token].desktop.send(JSON.stringify({ type: 'nextLesson', content: nextLessonContent }));
|
||
|
connections[token].mobile.send(JSON.stringify({ type: 'nextLesson', content: nextLessonContent }));
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// This assumes desktop connections come first
|
||
|
for (const code in connections) {
|
||
|
if (connections[code].desktop === null) {
|
||
|
connections[code].desktop = ws;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (session && session.data.user && session.data.user.role === 'admin') {
|
||
|
const confirmationCode = uuidv4().split('-')[0]; // Simple confirmation code
|
||
|
connections[confirmationCode] = { desktop: ws, mobile: null };
|
||
|
ws.send(JSON.stringify({ type: 'confirmationCode', code: confirmationCode }));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
const lessonData = [
|
||
|
{
|
||
|
title: 'Lesson 1',
|
||
|
text: 'This is the content for lesson 1.',
|
||
|
video: 'lesson1.mp4',
|
||
|
quiz: { question: 'What is 1 + 1?', answer: '2', choices: ['A', 'B', 'C', 'D'] }
|
||
|
},
|
||
|
{
|
||
|
title: 'Lesson 2',
|
||
|
text: 'This is the content for lesson 2.',
|
||
|
video: 'lesson2.mp4',
|
||
|
quiz: { question: 'What is 2 + 2?', answer: '4', choices: ['A', 'B', 'C', 'D'] }
|
||
|
},
|
||
|
{
|
||
|
title: 'Lesson 3',
|
||
|
text: 'This is the content for lesson 3.',
|
||
|
video: 'lesson3.mp4',
|
||
|
quiz: { question: 'What is 3 + 3?', answer: '6', choices: ['A', 'B', 'C', 'D'] }
|
||
|
},
|
||
|
{
|
||
|
title: 'Lesson 4',
|
||
|
text: 'This is the content for lesson 4.',
|
||
|
video: 'lesson4.mp4',
|
||
|
quiz: { question: 'What is 4 + 4?', answer: '8', choices: ['A', 'B', 'C', 'D'] }
|
||
|
},
|
||
|
{
|
||
|
title: 'Lesson 5',
|
||
|
text: 'This is the content for lesson 5.',
|
||
|
video: 'lesson5.mp4',
|
||
|
quiz: { question: 'What is 5 + 5?', answer: '10', choices: ['A', 'B', 'C', 'D'] }
|
||
|
}
|
||
|
];
|
||
|
let currentLesson = 0;
|
||
|
|
||
|
function getCurrentLesson() {
|
||
|
return lessonData[currentLesson];
|
||
|
}
|
||
|
|
||
|
function nextLesson() {
|
||
|
currentLesson = (currentLesson + 1) % lessonData.length;
|
||
|
return getCurrentLesson();
|
||
|
}
|
||
|
|
||
|
server.listen(port, () => {
|
||
|
console.log(`Server is running on port ${port}`);
|
||
|
nms.run(); // Start the Node-Media-Server
|
||
|
});
|