diff --git a/LICENCE.md b/LICENCE.md index 95a6ae4..e6f7a9d 100644 --- a/LICENCE.md +++ b/LICENCE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Dewilde Alexandre, Dubois Brieuc and Fischer Nicolas +Copyright (c) 2020 Dewilde Alexandre, Dubois Brieuc and Technicguy Theo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Privacy Policy - CodeWe.docx b/Privacy Policy - CodeWe.docx deleted file mode 100644 index b4cb012..0000000 Binary files a/Privacy Policy - CodeWe.docx and /dev/null differ diff --git a/Terms and Conditions - CodeWe.docx b/Terms and Conditions - CodeWe.docx deleted file mode 100644 index 3919a63..0000000 Binary files a/Terms and Conditions - CodeWe.docx and /dev/null differ diff --git a/src/app.js b/src/app.js index b150310..9e401e2 100644 --- a/src/app.js +++ b/src/app.js @@ -16,10 +16,22 @@ const index = require('./routes/index'); const editor = require('./routes/editor'); const legal = require('./routes/legal'); const config = require('./config/config'); +const ssl = config.SSL; const app = express(); app.disable("x-powered-by"); +if (ssl) { + app.use('*', function (req, res, next) { + if (req.secure) { + next(); + } else { + const target = (req.headers.host.includes(':') ? req.headers.host.split(':')[0] : req.headers.host) + ':' + config.PORT; + res.redirect('https://' + target + req.url); + } + }); +} + // Configure views folder nunjucks.configure(path.join(__dirname, 'views'), { autoescape: true, @@ -51,7 +63,6 @@ app.use('/', index); app.use('/editor', editor); app.use('/legal', legal); - // 404 error app.all('*', (req, res) => { res.status(404).render('404.html', {production: config.PRODUCTION, client_versobe: config.CLIENT_VERBOSE}); diff --git a/src/config/config dist.json b/src/config/config dist.json index fd4ec00..4a547f3 100644 --- a/src/config/config dist.json +++ b/src/config/config dist.json @@ -1,21 +1,16 @@ { "DEBUG" : true, - "HOST": "localhost", - "PORT": 5000, - "DB_TYPE": "mysql", "PRODUCTION": false, "CLIENT_VERBOSE": 2, + "HOST": "localhost", + "PORT": 5000, + "DB_URL": "mongodb://host:port/?retryWrites=true&w=majority", + "DAYS_TO_DELETE_DOCUMENT": 7, "DISCORD_WEBHOOK": null, "SSL": false, "KEY_FILE_SSL": null, "CERT_FILE_SSL" : null, - "DB_CONFIG": { - "DB_HOST": "db", - "DB_USERNAME": "root", - "DB_PASSWORD": "root", - "DB_DATABASE": "codewe", - "DB_PORT": "3306" - }, + "REDIRECT_PORT": null, "METRICS": false, "METRICS_PORT": 8000 } diff --git a/src/db/MongoDB.js b/src/db/MongoDB.js index 7497b92..2a86831 100644 --- a/src/db/MongoDB.js +++ b/src/db/MongoDB.js @@ -1,4 +1,5 @@ const { MongoClient, ObjectID } = require("mongodb"); +var crypto = require('crypto'); const configs = require('../config/config'); const utils = require('../utils'); @@ -12,9 +13,11 @@ const baseCode = [ class MongoDB { - constructor (username, password, host, database, port) { - let url = `mongodb://${username}:${password}@${host}:${port}/?retryWrites=true&w=majority`; - this.client = new MongoClient(url); + constructor (url) { + this.client = new MongoClient(url, { + useNewUrlParser: true, + useUnifiedTopology: true + }); } async connect () { @@ -22,6 +25,7 @@ class MongoDB { this.db = await this.client.connect(); this.codeWe = await this.db.db('codewe'); this.documentsCollection = await this.codeWe.collection('codewe'); + this.usersCollection = await this.codeWe.collection('users'); } catch (err) { if (configs.DEBUG) { console.error('Error with db connection'); @@ -30,7 +34,7 @@ class MongoDB { } } - async createDocument () { + async createDocument (language) { let doc = { content: baseCode, creationDate: Date.now(), @@ -38,21 +42,21 @@ class MongoDB { customDocumentName: '', documentOwner: '', editors: [], - linkEdit: '', + documentLink: '', linkView: '', - language: '', + language: language, tab: 4 }; try { let results = (await this.documentsCollection.insertOne(doc)); const documentLink = utils.uuid(results.insertedId.toString()); - this.documentsCollection.updateOne({_id: results.insertedId}, {$set: {documentLink: documentLink}}) + const linkView = utils.uuid(documentLink); + this.documentsCollection.updateOne({_id: results.insertedId}, {$set: {documentLink: documentLink, linkView: linkView}}); return documentLink; } catch (err) { if (configs.DEBUG) { console.error('Error when creating a new document'); } - throw new Error(err); } } @@ -64,43 +68,70 @@ class MongoDB { if (configs.DEBUG) { console.error('Error when fetching document'); } - throw new Error(err); + } + } + + async createUser(userId, secretToken) { + try { + await this.usersCollection.insertOne({ + userId: userId, + secretToken: crypto.createHash('sha256').update(secretToken).digest('base64') + }); + return 'Success'; + } catch (err) { + if (configs.DEBUG) { + console.error('Error when creating user'); + } + } + } + + async checkUserSecretToken(userId, secretToken) { + try { + const user = await this.usersCollection.findOne({userId: userId}); + return (user.secretToken == crypto.createHash('sha256').update(secretToken).digest('base64')); + } catch (err) { + if (configs.DEBUG) { + console.error('Error when checking user secret token'); + } + return 'Error'; } } async setLine (documentLink, uuid, content) { try { - await this.documentsCollection.updateOne({documentLink: documentLink, 'content.uuid': uuid}, {$set: {'content.$.content': content}}); + await this.documentsCollection.updateOne({documentLink: documentLink, 'content.uuid': uuid}, {$set: {'content.$.content': content.slice(0, 5000)}}); + return 'Succes'; } catch (err) { if (configs.DEBUG) { console.error('Error when changing line content'); } - throw new Error(err); } } async newLine (documentLink, previousUuid, uuid, content) { // Insert a line at the right place - //TODO is it possible in one operation ? + //TODO is it possible in one operation ? // TODO is it possible to implement with bulk? try { let doc = await this.documentsCollection.findOne({documentLink: documentLink}); let index = doc.content.findIndex(line => { return line.uuid == previousUuid; }); - this.documentsCollection.updateOne({documentLink: documentLink}, { - $push: { - content: { - $each : [{uuid: uuid, content: content}], - $position : index + 1 + if (index) { + this.documentsCollection.updateOne({documentLink: documentLink}, { + $push: { + content: { + $each : [{uuid: uuid, content: content.slice(0, 5000)}], + $position : index + 1 + } } - } - }); + }); + } + return 'Succes'; } catch (err) { if (configs.DEBUG) { console.error('Error when adding a new line to document'); } - throw new Error(err); } } @@ -108,50 +139,100 @@ class MongoDB { try { // Delete line at the right place await this.documentsCollection.updateOne({documentLink: documentLink}, {$pull: {content: {uuid: uuid}}}); + return 'Succes'; } catch (err) { if (configs.DEBUG) { console.error('Error when deleting a line in document'); } - throw new Error(err); } } + async changeParam(documentLink, param, newValue) { + try { + const update = {}; + update[param] = newValue; + await this.documentsCollection.updateOne({documentLink: documentLink}, {$set: update}); + return 'Succes'; + } catch (err) { + if (configs.DEBUG) { + console.error(err); + } + } + } + + async changeCustomName(documentLink, newName) { + return this.changeParam(documentLink, 'customDocumentName', newName); + } + + async changeTabSize(documentLink, newTabSize) { + if (Number.isInteger(newTabSize)) { + return this.changeParam(documentLink, 'tab', newTabSize); + } + } + + async changeLanguage(documentLink, newLanguage) { + if (["python"].includes(newLanguage)) { + return this.changeParam(documentLink, 'language', newLanguage); + } + } + + async addNewEditors(documentLink, newEditorsId) { + try { + await this.documentsCollection.updateOne({documentLink: documentLink}, {$addToSet: {editors: newEditorsId}}); + return 'Success'; + } catch (err) { + if (configs.DEBUG) { + console.error(error); + } + } + } + + async updateLastViewedDate(documentLink) { + return this.changeParam(documentLink, 'lastViewedDate', Date.now()); + } + + async deleteOldDocuments(days) { + const oldTimestamp = Date.now() - 1000 * 60 * 60 * 24 * days; + return this.documentsCollection.deleteMany({'lastViewedDate': {$lt : oldTimestamp} }); + } + async applyRequests (documentLink, requests) { // TODO look to use bulk write + let success = true; try { + // Avoid too many requests + requests = requests.slice(0, 50); for (let request of requests) { let requestType = request.type; let data = request.data; + let results = "" switch (requestType) { case 'set-line': - await this.setLine(documentLink, data.id, data.content); + results = await this.setLine(documentLink, data.id, data.content); + if (!results) success = false; break; case 'new-line': - await this.newLine(documentLink, data.previous, data.id, data.content); + results = await this.newLine(documentLink, data.previous, data.id, data.content); + if (!results) success = false; break; case 'delete-line': - await this.deleteLine(documentLink, data.id); + results = await this.deleteLine(documentLink, data.id); + if (!results) success = false; break; } } + return success; } catch (err) { if (configs.DEBUG) { console.error('Error when applying requests'); } - throw new Error(err); } } } function getDB () { - db = new MongoDB( - configs.DB_CONFIG.DB_USERNAME, - configs.DB_CONFIG.DB_PASSWORD, - configs.DB_CONFIG.DB_HOST, - configs.DB_CONFIG.DB_DATABASE, - configs.DB_CONFIG.DB_PORT - ); + db = new MongoDB(configs.DB_URL); db.connect(); return db; } diff --git a/src/publics/js/dev/page/editor/editable.js b/src/publics/js/dev/page/editor/editable.js index 71ed89f..36778ea 100644 --- a/src/publics/js/dev/page/editor/editable.js +++ b/src/publics/js/dev/page/editor/editable.js @@ -137,16 +137,18 @@ export default class Editable{ const line = getNodeFromAttribute('uuid'); - if(!line) return; + if(!line){ + e.preventDefault(); + temporaryCardAlert('Editor', 'Sorry, your action has been canceled because you are not on any line.', 5000); + return; + } if(!anchorParent.hasAttribute('uuid') || !focusParent.hasAttribute('uuid') || ((Caret.getBeginPosition(line) === 0 || Caret.getEndPosition(line) === 0) ) && anchorParent !== focusParent){ - e.preventDefault(); - temporaryCardAlert('Override', 'Sorry, you can\'t override the first char of a line', 5000); - return; + Caret.setRangeStart(line, 1); } switch (e.keyCode) { @@ -155,6 +157,11 @@ export default class Editable{ this.insertTab(); break; case 13: // enter + if(e.shiftKey){ + temporaryCardAlert('Shift+Enter', 'Please just use Enter to avoid any bugs.', 5000); + e.preventDefault(); + return; + } if(this.keepSpace){ Debug.debug('Prevent action when trying to add new line (key is probably maintain).'); e.preventDefault(); diff --git a/src/publics/js/dev/utils/caret.js b/src/publics/js/dev/utils/caret.js index 4799907..534a543 100644 --- a/src/publics/js/dev/utils/caret.js +++ b/src/publics/js/dev/utils/caret.js @@ -67,6 +67,22 @@ export default class Caret{ } } + + /** + * Set the start range of the user caret on specified position in element or children + * @param {HTMLElement|Node} element + * @param {number} position + */ + static setRangeStart(element, position) { + if (position >= 0) { + let selection = document.getSelection(); + + let range = Caret.createRange(element, {count: position}); + selection.getRangeAt(0).setStart(range.endContainer, range.endOffset); + + } + } + /** * Get the position of the end of the user selection * Based on https://stackoverflow.com/a/4812022/11247647 diff --git a/src/routes/editor.js b/src/routes/editor.js index 4f22964..1431290 100644 --- a/src/routes/editor.js +++ b/src/routes/editor.js @@ -41,10 +41,11 @@ const router = express.Router(); router.get('/:docId', async (req, res, next) => { try { let document = (await db.getDocument(req.params.docId)); - if (document) { + if (document) { // && (document.public || (document.editors.includes(req.body.userId) && db.checkUserSecretToken(req.body.userId, secretkey))) document.document_id = req.params.docId; res.render('editor.html', {document: document, production: config.PRODUCTION, client_versobe: config.CLIENT_VERBOSE}); } + // else if (!document.public) else { res.status(404).render('404.html', {production: config.PRODUCTION, client_versobe: config.CLIENT_VERBOSE}) } diff --git a/src/routes/index.js b/src/routes/index.js index 9d0ccb7..da1c297 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -52,8 +52,14 @@ router.get('/', (req, res) => { */ router.post('/create_document', async (req, res, next) => { try { - let documentId = await db.createDocument(); - res.redirect(`/editor/${documentId}`); + //const language = req.body.language + let documentId = await db.createDocument('python'); + if (documentId) { + res.redirect(`/editor/${documentId}`); + } + else { + res.status(500); + } } catch (err) { next(err); } diff --git a/src/routes/legal.js b/src/routes/legal.js index 60bab8f..0da9949 100644 --- a/src/routes/legal.js +++ b/src/routes/legal.js @@ -8,7 +8,7 @@ * */ - /** +/** * express module * @const */ @@ -22,14 +22,28 @@ const config = require('../config/config'); const router = express.Router(); /** -* Route serving termsofservice -* @name get/termsofservice -* @function -* @memberof modules:routes/legal -* @inner -*/ + * Route serving termsofservice + * @name get/termsofservice + * @function + * @memberof modules:routes/legal + * @inner + */ router.get(['/tos', '/tac', '/termsofservice', '/terms-of-service'], (req, res) => { - res.render('legal/tos.html', {production: config.PRODUCTION, client_versobe: config.CLIENT_VERBOSE}); + res.render('legal/tos.html', { + production: config.PRODUCTION, + client_versobe: config.CLIENT_VERBOSE + }); +}); + +/** + * Route serving termsofservice PDF + * @name get/termsofservice + * @function + * @memberof modules:routes/legal + * @inner + */ +router.get(['/tos-pdf', '/tac-pdf', '/termsofservice-pdf', '/terms-of-service-pdf'], (req, res) => { + res.download("./views/legal/tos-pdf.pdf") }); /** @@ -59,7 +73,21 @@ router.get([ * @inner */ router.get(['/privacy', '/privacy-policy', '/privacypolicy'], (req, res) => { - res.render('legal/privacy.html', {production: config.PRODUCTION, client_versobe: config.CLIENT_VERBOSE}); + res.render('legal/privacy.html', { + production: config.PRODUCTION, + client_versobe: config.CLIENT_VERBOSE + }); +}); + +/** + * Route serving privacy policy PDF + * @name get/privacy + * @function + * @memberof modules:routes/legal + * @inner + */ +router.get(['/privacy-pdf', '/privacy-policy-pdf', '/privacypolicy-pdf'], (req, res) => { + res.download("./views/legal/privacy-pdf.pdf") }); /** @@ -70,7 +98,10 @@ router.get(['/privacy', '/privacy-policy', '/privacypolicy'], (req, res) => { * @inner */ router.get(['/privacy/archive/:date', '/privacy-policy/archive/:date', '/privacypolicy/archive/:date'], (req, res) => { - res.render(`legal/archive/privacy-${req.params.date}.html`, {production: config.PRODUCTION, client_versobe: config.CLIENT_VERBOSE}); + res.render(`legal/archive/privacy-${req.params.date}.html`, { + production: config.PRODUCTION, + client_versobe: config.CLIENT_VERBOSE + }); }); /** @@ -80,8 +111,11 @@ router.get(['/privacy/archive/:date', '/privacy-policy/archive/:date', '/privacy * @memberof modules:routes/legal * @inner */ -router.get('/license', (req, res) => { - res.render('legal/licence.html', {production: config.PRODUCTION, client_versobe: config.CLIENT_VERBOSE}); +router.get('/licence', (req, res) => { + res.render('legal/licence.html', { + production: config.PRODUCTION, + client_versobe: config.CLIENT_VERBOSE + }); }); module.exports = router; diff --git a/src/server.js b/src/server.js index d3b1541..9042037 100644 --- a/src/server.js +++ b/src/server.js @@ -3,7 +3,7 @@ * @author Alexandre Dewilde * @date 15/11/2020 * @version 1.0.0 - * + * */ const fs = require('fs'); const path = require('path'); @@ -14,8 +14,9 @@ const host = configs.HOST; const port = configs.PORT; const DEBUG = configs.DEBUG; const ssl = configs.SSL; +const metrics = configs.METRICS; const sslKeyPath = configs.KEY_FILE_SSL; -const sslCertPath = configs.CERT_FILE_SSL; +const sslCertPath = configs.CERT_FILE_SSL; const options = ssl ? { key: fs.readFileSync(sslKeyPath.startsWith('/') ? sslKeyPath : path.join(__dirname, sslKeyPath), 'utf8'), @@ -27,9 +28,11 @@ const config = require('./config/config'); const http = ssl ? require('https') : require('http'); const server = http.createServer(options, app); -if (configs.METRICS) { - const {metricsApp} = require('./metricsApp'); - var metricsServer = http.createServer(metricsApp); + +if (ssl && configs.REDIRECT_PORT !== null) { + require('http').createServer(app).listen(configs.REDIRECT_PORT, host, () => { + console.log(`http requests from ${configs.REDIRECT_PORT} are redirected to https on ${configs.PORT}`) + }) } // config websockets @@ -39,9 +42,11 @@ require('./socket/socket')(wss); server.listen(port, host, () => { - console.log('Server Started!'); + console.log(`Server Started on ${host}:${port}`); }); -if (config.METRICS) { +if (metrics) { + const {metricsApp} = require('./metricsApp'); + const metricsServer = require('http').createServer(metricsApp); metricsServer.listen(config.METRICS_PORT); } diff --git a/src/socket/socket.js b/src/socket/socket.js index 27568b8..8ae3ac0 100644 --- a/src/socket/socket.js +++ b/src/socket/socket.js @@ -32,6 +32,19 @@ module.exports = function (wss) { socket.isAlive = true; const uuid = utils.uuid(Math.random().toString()); + const broadcastRoomExceptSender = (data, event, valueEvent) => { + Object.entries(rooms[data.room]).forEach(([, sock]) => { + if(sock === socket) { + const backValue = { + code: "OK", + time: Date.now(), + }; + backValue[event] = valueEvent; + sock.send(JSON.stringify(backValue)); + }else sock.send(JSON.stringify(data)); + }); + } + const leave = room => { if(! rooms[room][uuid]) return; // if the one exiting is the last one, destroy the room @@ -48,18 +61,17 @@ module.exports = function (wss) { switch (data.event) { case 'update': try { - Object.entries(rooms[data.room]).forEach(([, sock]) => { - if(sock === socket) { - sock.send(JSON.stringify({ - code: "OK", - uuid: data['uuid'], - time: Date.now(), - })); - }else sock.send(JSON.stringify(data)); - }); - db.applyRequests(data.room, data.data); + // let document = db.getDocument(data.room); + // if (document.public || (document.editors.include(userId) and db.checkUsersSecretToken(userId, secretToken))) + broadcastRoomExceptSender(data, 'uuid', data.uuid); + const succesUpdatingDate = db.updateLastViewedDate(data.room); + const succesUpdate = db.applyRequests(data.room, data.data); + // /!\ Bad event + // if (!succesUpdatingDate || !succesUpdate) socket.send(JSON.stringify({event: 'update', success: false})); } catch (err) { - throw new Error(err); + if (config.DEBUG) { + console.error(err); + } } break; case 'join': @@ -71,6 +83,40 @@ module.exports = function (wss) { rooms[data.room][uuid] = socket; } break; + + case 'language': + try { + broadcastRoomExceptSender(data, 'language', data.language); + const success = db.changeLanguage(data.room, data.language); + if (!success) socket.send(JSON.stringify({event: 'language', success: false})); + } catch (err) { + if (config.DEBUG) { + console.error(err); + } + } + break; + case 'changeTabSize': + try { + broadcastRoomExceptSender(data, 'tabSize', data.tabSize); + const success = db.changeTabSize(data.room, data.tabSize); + if (!success) socket.send(JSON.stringify({event: 'changeTabSize', success: false})); + } catch (err) { + if (config.DEBUG) { + console.error(err); + } + } + break; + case 'changeCustomName': + try { + broadcastRoomExceptSender(data, 'customName', data.customName); + let success = db.changeCustomName(data.customName); + if (!success) socket.send(JSON.stringify({event: 'changeCustomName', success: false})); + } catch (err) { + if (config.DEBUG) { + console.error(err); + } + } + break; case 'ping': socket.send('pong'); break; @@ -79,7 +125,7 @@ module.exports = function (wss) { break; case 'report': // Send issue to hook if (hook) { - hook.warn('Report', data.data.content); + hook.warn('Report', data.data.content.slice(0, 5000)); } } @@ -117,5 +163,10 @@ module.exports = function (wss) { prom.connected.set(wss.clients.size); }, 5000); + // delete old documents + setInterval(() => { + db.deleteOldDocuments(config.DAYS_TO_DELETE_DOCUMENT); + }, 1000 * 60 * 60 * 24); + } diff --git a/src/views/component/footer.html b/src/views/component/footer.html index 38725c2..9c3629c 100644 --- a/src/views/component/footer.html +++ b/src/views/component/footer.html @@ -8,7 +8,7 @@ Support Report issue - Copyright © 2020 Dewilde Alexandre, Dubois Brieuc and Fischer Nicolas + Copyright © 2020 Dewilde Alexandre, Dubois Brieuc and Technicguy Theo