diff --git a/README.md b/README.md index 6762b28a0..ab87217c6 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,70 @@ -HackMD 0.2.9 +HackMD 0.3.1 === -HackMD is a realtime collaborative markdown notes on all platforms. -Inspired by Hackpad, but more focusing on speed and flexibility. -Still in early stage, feel free to fork or contribute to this. +HackMD is a realtime collaborative markdown notes on all platforms. +Inspired by Hackpad, but more focusing on speed and flexibility. +Still in early stage, feel free to fork or contribute to this. Thanks for your using! :smile: -Dependency +Database dependency --- - PostgreSQL 9.3.6 or 9.4.1 - MongoDB 3.0.2 -Import db schema +Import database schema --- -The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql` +The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql` To import the sql file in PostgreSQL, type `psql -i hackmd_schema.sql` -The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string. +The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string. -Config +Structure +--- +``` +hackmd/ +├── logs/ --- server logs +├── backups/ --- db backups +├── tmp/ --- temporary files +├── lib/ --- server libraries +└── public/ --- client files + ├── css/ --- css styles + ├── js/ --- js scripts + ├── vendor/ --- vendor includes + └── views/ --- view templates +``` + +Configure --- There are some config you need to change in below files ``` -./run.sh -./config.js -./public/js/common.js +./Procfile --- for heroku start +./run.sh --- for forever start +./processes.json --- for pm2 start +./config.js --- for server settings +./public/js/common.js --- for client settings +./hackmd --- for logrotate ``` -The script `run.sh`, it's for someone like me to run the server via npm package `forever`, and can passing environment variable to the server, like heroku does. +**From 0.3.1, we no longer recommend using `forever` to run your server.** -To install `forever`, just type `npm install forever -g` +We using `pm2` to run server. +See [here](https://github.com/Unitech/pm2) for details. You can use SSL to encrypt your site by passing certificate path in the `config.js` and set `usessl=true` Run a server --- -To run the server, type `bash run.sh` -Log will be at `~/.forever/hackmd.log` + - forever: `bash run.sh` + - pm2: `pm2 start processes.json` Stop a server --- -To stop the server, simply type `forever stop hackmd` + - forever: `forever stop hackmd` + - pm2: `pm2 stop hackmd` Backup db --- -To backup the db, type `bash backup.sh` -Backup files will be at `./backups/` - +To backup the db, type `bash backup.sh` **License under MIT.** \ No newline at end of file diff --git a/app.js b/app.js index c52869743..c2ff400d1 100644 --- a/app.js +++ b/app.js @@ -49,13 +49,15 @@ if (config.usessl) { var app = express(); var server = require('http').createServer(app); } -//socket io listen -var io = require('socket.io').listen(server); + //logger app.use(morgan('combined', { "stream": logger.stream })); +//socket io +var io = require('socket.io')(server); + // connect to the mongodb mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring); @@ -80,7 +82,7 @@ var sessionStore = new MongoStore({ touchAfter: config.sessiontouch }, function (err) { - console.log(err); + logger.info(err); }); //compression @@ -115,12 +117,12 @@ app.use(passport.session()); //serialize and deserialize passport.serializeUser(function (user, done) { - //console.log('serializeUser: ' + user._id); + //logger.info('serializeUser: ' + user._id); done(null, user._id); }); passport.deserializeUser(function (id, done) { User.model.findById(id, function (err, user) { - //console.log(user) + //logger.info(user) if (!err) done(null, user); else done(err, null); }) @@ -163,7 +165,7 @@ app.get("/temp", function (req, res) { }); temp.remove(function (err) { if (err) - console.log('remove temp failed: ' + err); + logger.error('remove temp failed: ' + err); }); } }); @@ -182,7 +184,7 @@ app.post("/temp", urlencodedParser, function (req, res) { response.errorForbidden(res); else { if (config.debug) - console.log('SERVER received temp from [' + host + ']: ' + req.body.data); + logger.info('SERVER received temp from [' + host + ']: ' + req.body.data); Temp.newTemp(id, data, function (err, temp) { if (!err && temp) { res.header("Access-Control-Allow-Origin", "*"); @@ -247,7 +249,7 @@ app.get('/auth/dropbox/callback', //logout app.get('/logout', function (req, res) { if (config.debug && req.session.passport.user) - console.log('user logout: ' + req.session.passport.user); + logger.info('user logout: ' + req.session.passport.user); req.logout(); res.redirect('/'); }); @@ -256,7 +258,7 @@ app.get('/history', function (req, res) { if (req.isAuthenticated()) { User.model.findById(req.session.passport.user, function (err, user) { if (err) { - console.log('read history failed: ' + err); + logger.error('read history failed: ' + err); } else { var history = []; if (user.history) @@ -274,18 +276,18 @@ app.get('/history', function (req, res) { app.post('/history', urlencodedParser, function (req, res) { if (req.isAuthenticated()) { if (config.debug) - console.log('SERVER received history from [' + req.session.passport.user + ']: ' + req.body.history); + logger.info('SERVER received history from [' + req.session.passport.user + ']: ' + req.body.history); User.model.findById(req.session.passport.user, function (err, user) { if (err) { - console.log('write history failed: ' + err); + logger.error('write history failed: ' + err); } else { user.history = req.body.history; user.save(function (err) { if (err) { - console.log('write user history failed: ' + err); + logger.error('write user history failed: ' + err); } else { if (config.debug) - console.log("write user history success: " + user._id); + logger.info("write user history success: " + user._id); }; }); } @@ -300,7 +302,7 @@ app.get('/me', function (req, res) { if (req.isAuthenticated()) { User.model.findById(req.session.passport.user, function (err, user) { if (err) { - console.log('read me failed: ' + err); + logger.error('read me failed: ' + err); } else { var profile = JSON.parse(user.profile); res.send({ @@ -324,20 +326,25 @@ app.post('/uploadimage', function (req, res) { response.errorForbidden(res); } else { if (config.debug) - console.log('SERVER received uploadimage: ' + JSON.stringify(files.image)); + logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)); imgur.setClientId(config.imgur.clientID); - imgur.uploadFile(files.image.path) - .then(function (json) { - if (config.debug) - console.log('SERVER uploadimage success: ' + JSON.stringify(json)); - res.send({ - link: json.data.link + try { + imgur.uploadFile(files.image.path) + .then(function (json) { + if (config.debug) + logger.info('SERVER uploadimage success: ' + JSON.stringify(json)); + res.send({ + link: json.data.link + }); + }) + .catch(function (err) { + logger.error(err); + res.send('upload image error'); }); - }) - .catch(function (err) { - console.error(err); - res.send(err.message); - }); + } catch (err) { + logger.error(err); + res.send('upload image error'); + } } }); }); @@ -345,6 +352,10 @@ app.post('/uploadimage', function (req, res) { app.get("/new", response.newNote); //get features app.get("/features", response.showFeatures); +//get share note +app.get("/s/:shortid", response.showShareNote); +//share note actions +app.get("/s/:shortid/:action", response.shareNoteActions); //get note by id app.get("/:noteId", response.showNote); //note actions @@ -370,10 +381,10 @@ io.sockets.on('connection', realtime.connection); //listen if (config.usessl) { server.listen(config.sslport, function () { - console.log('HTTPS Server listening at sslport %d', config.sslport); + logger.info('HTTPS Server listening at sslport %d', config.sslport); }); } else { server.listen(config.port, function () { - console.log('HTTP Server listening at port %d', config.port); + logger.info('HTTP Server listening at port %d', config.port); }); } \ No newline at end of file diff --git a/backups/.keep b/backups/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/config.js b/config.js index 03764fef7..bf3350688 100644 --- a/config.js +++ b/config.js @@ -11,7 +11,7 @@ var urladdport = true; //add port on getserverurl var config = { debug: true, - version: '0.2.8', + version: '0.3.1', domain: domain, alloworigin: ['add here to allow origin to cross'], testport: testport, diff --git a/hackmd b/hackmd new file mode 100644 index 000000000..9005e2aad --- /dev/null +++ b/hackmd @@ -0,0 +1,11 @@ +/home/hackmd/logs/*.log { + daily + rotate 365 + missingok + notifempty + compress + sharedscripts + copytruncate + dateext + dateformat %Y-%m-%d +} \ No newline at end of file diff --git a/lib/auth.js b/lib/auth.js index 456c9dff0..dc8b94cad 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -7,17 +7,18 @@ var GithubStrategy = require('passport-github').Strategy; var DropboxStrategy = require('passport-dropbox-oauth2').Strategy; //core -var User = require('./user.js') -var config = require('../config.js') +var User = require('./user.js'); +var config = require('../config.js'); +var logger = require("./logger.js"); function callback(accessToken, refreshToken, profile, done) { - //console.log(profile.displayName || profile.username); + //logger.info(profile.displayName || profile.username); User.findOrNewUser(profile.id, profile, function (err, user) { if (err || user == null) { - console.log('auth callback failed: ' + err); + logger.error('auth callback failed: ' + err); } else { - if(config.debug && user) - console.log('user login: ' + user._id); + if (config.debug && user) + logger.info('user login: ' + user._id); done(null, user); } }); diff --git a/lib/db.js b/lib/db.js index d924cdf83..e0f873e51 100644 --- a/lib/db.js +++ b/lib/db.js @@ -6,6 +6,7 @@ var util = require('util'); //core var config = require("../config.js"); +var logger = require("./logger.js"); //public var db = { @@ -48,18 +49,18 @@ function newToDB(id, owner, body, callback) { if (err) { client.end(); callback(err, null); - return console.error('could not connect to postgres', err); + return logger.error('could not connect to postgres', err); } var newnotequery = util.format(insertquery, id, owner, body); - //console.log(newnotequery); + //logger.info(newnotequery); client.query(newnotequery, function (err, result) { client.end(); if (err) { callback(err, null); - return console.error("new note to db failed: " + err); + return logger.error("new note to db failed: " + err); } else { if (config.debug) - console.log("new note to db success"); + logger.info("new note to db success"); callback(null, result); } }); @@ -72,22 +73,22 @@ function readFromDB(id, callback) { if (err) { client.end(); callback(err, null); - return console.error('could not connect to postgres', err); + return logger.error('could not connect to postgres', err); } var readquery = util.format(selectquery, id); - //console.log(readquery); + //logger.info(readquery); client.query(readquery, function (err, result) { client.end(); if (err) { callback(err, null); - return console.error("read from db failed: " + err); + return logger.error("read from db failed: " + err); } else { - //console.log(result.rows); + //logger.info(result.rows); if (result.rows.length <= 0) { callback("not found note in db: " + id, null); } else { if(config.debug) - console.log("read from db success"); + logger.info("read from db success"); callback(null, result); } } @@ -101,18 +102,18 @@ function saveToDB(id, title, data, callback) { if (err) { client.end(); callback(err, null); - return console.error('could not connect to postgres', err); + return logger.error('could not connect to postgres', err); } var savequery = util.format(updatequery, title, data, id); - //console.log(savequery); + //logger.info(savequery); client.query(savequery, function (err, result) { client.end(); if (err) { callback(err, null); - return console.error("save to db failed: " + err); + return logger.error("save to db failed: " + err); } else { if (config.debug) - console.log("save to db success"); + logger.info("save to db success"); callback(null, result); } }); @@ -125,20 +126,20 @@ function countFromDB(callback) { if (err) { client.end(); callback(err, null); - return console.error('could not connect to postgres', err); + return logger.error('could not connect to postgres', err); } client.query(countquery, function (err, result) { client.end(); if (err) { callback(err, null); - return console.error("count from db failed: " + err); + return logger.error("count from db failed: " + err); } else { - //console.log(result.rows); + //logger.info(result.rows); if (result.rows.length <= 0) { callback("not found note in db", null); } else { if(config.debug) - console.log("count from db success"); + logger.info("count from db success"); callback(null, result); } } diff --git a/lib/logger.js b/lib/logger.js index c843330b2..61299c10d 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -7,7 +7,8 @@ var logger = new winston.Logger({ level: 'debug', handleExceptions: true, json: false, - colorize: true + colorize: true, + timestamp: true }) ], exitOnError: false diff --git a/lib/note.js b/lib/note.js index 1212e1a6c..dc384b7fe 100644 --- a/lib/note.js +++ b/lib/note.js @@ -1,22 +1,56 @@ //note //external modules +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; var LZString = require('lz-string'); var marked = require('marked'); var cheerio = require('cheerio'); +var shortId = require('shortid'); //others var db = require("./db.js"); +var logger = require("./logger.js"); + +//permission types +permissionTypes = ["freely", "editable", "locked"]; + +// create a note model +var model = mongoose.model('note', { + id: String, + shortid: { + type: String, + unique: true, + default: shortId.generate + }, + permission: { + type: String, + enum: permissionTypes + }, + viewcount: { + type: Number, + default: 0 + }, + updated: Date, + created: Date +}); //public var note = { + model: model, + findNote: findNote, + newNote: newNote, + findOrNewNote: findOrNewNote, checkNoteIdValid: checkNoteIdValid, checkNoteExist: checkNoteExist, - getNoteTitle: getNoteTitle + getNoteTitle: getNoteTitle, + generateWebTitle: generateWebTitle, + increaseViewCount: increaseViewCount, + updatePermission: updatePermission }; function checkNoteIdValid(noteId) { try { - //console.log(noteId); + //logger.info(noteId); var id = LZString.decompressFromBase64(noteId); if (!id) return false; var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; @@ -26,21 +60,21 @@ function checkNoteIdValid(noteId) { else return false; } catch (err) { - console.error(err); + logger.error(err); return false; } } function checkNoteExist(noteId) { try { - //console.log(noteId); + //logger.info(noteId); var id = LZString.decompressFromBase64(noteId); db.readFromDB(id, function (err, result) { if (err) return false; return true; }); } catch (err) { - console.error(err); + logger.error(err); return false; } } @@ -50,11 +84,118 @@ function getNoteTitle(body) { var $ = cheerio.load(marked(body)); var h1s = $("h1"); var title = ""; - if (h1s.length > 0) + if (h1s.length > 0 && h1s.first().text().split('\n').length == 1) title = h1s.first().text(); else title = "Untitled"; return title; } +//generate note web page title +function generateWebTitle(title) { + title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD"; + return title; +} + +function findNote(id, callback) { + model.findOne({ + $or: [ + { + id: id + }, + { + shortid: id + } + ] + }, function (err, note) { + if (err) { + logger.error('find note failed: ' + err); + callback(err, null); + } + if (!err && note) { + callback(null, note); + } else { + logger.error('find note failed: ' + err); + callback(err, null); + }; + }); +} + +function newNote(id, permission, callback) { + var note = new model({ + id: id, + permission: permission, + updated: Date.now(), + created: Date.now() + }); + note.save(function (err) { + if (err) { + logger.error('new note failed: ' + err); + callback(err, null); + } else { + logger.info("new note success: " + note.id); + callback(null, note); + }; + }); +} + +function findOrNewNote(id, permission, callback) { + findNote(id, function (err, note) { + if (err || !note) { + newNote(id, permission, function (err, note) { + if (err) { + logger.error('find or new note failed: ' + err); + callback(err, null); + } else { + callback(null, note); + } + }); + } else { + if (!note.permission) { + note.permission = permission; + note.updated = Date.now(); + note.save(function (err) { + if (err) { + logger.error('add note permission failed: ' + err); + callback(err, null); + } else { + logger.info("add note permission success: " + note.id); + callback(null, note); + }; + }); + } else { + callback(null, note); + } + } + }); +} + +function increaseViewCount(note, callback) { + note.viewcount++; + note.updated = Date.now(); + note.save(function (err) { + if (err) { + logger.error('increase note viewcount failed: ' + err); + callback(err, null); + } else { + logger.info("increase note viewcount success: " + note.id); + callback(null, note); + }; + }); +} + +function updatePermission(note, permission, callback) { + note.permission = permission; + note.updated = Date.now(); + note.save(function (err) { + if (err) { + logger.error('update note permission failed: ' + err); + callback(err, null); + } else { + logger.info("update note permission success: " + note.id); + callback(null, note); + }; + }); +} + module.exports = note; \ No newline at end of file diff --git a/lib/realtime.js b/lib/realtime.js index 1a57c77ee..57038b725 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -9,9 +9,12 @@ var shortId = require('shortid'); var randomcolor = require("randomcolor"); var Chance = require('chance'), chance = new Chance(); +var md5 = require("blueimp-md5").md5; +var moment = require('moment'); //core var config = require("../config.js"); +var logger = require("./logger.js"); //others var db = require("./db.js"); @@ -49,7 +52,7 @@ function secure(socket, next) { next(new Error('AUTH failed: No cookie transmitted.')); } if (config.debug) - console.log("AUTH success cookie: " + handshakeData.sessionID); + logger.info("AUTH success cookie: " + handshakeData.sessionID); next(); } catch (ex) { @@ -65,9 +68,10 @@ var updater = setInterval(function () { var note = notes[key]; if (note.isDirty) { if (config.debug) - console.log("updater found dirty note: " + key); + logger.info("updater found dirty note: " + key); var body = LZString.decompressFromUTF16(note.body); var title = Note.getNoteTitle(body); + title = LZString.compressToBase64(title); body = LZString.compressToBase64(body); db.saveToDB(key, title, body, function (err, result) {}); @@ -75,36 +79,45 @@ var updater = setInterval(function () { } callback(); }, function (err) { - if (err) return console.error('updater error', err); + if (err) return logger.error('updater error', err); }); }, 5000); function getStatus(callback) { db.countFromDB(function (err, data) { - if (err) return console.log(err); - var regusers = 0; - var distinctregusers = 0; + if (err) return logger.info(err); var distinctaddresses = []; + var regaddresses = []; + var distinctregaddresses = []; Object.keys(users).forEach(function (key) { - var value = users[key]; - if (value.login) - regusers++; + var user = users[key]; var found = false; for (var i = 0; i < distinctaddresses.length; i++) { - if (value.address == distinctaddresses[i]) { + if (user.address == distinctaddresses[i]) { found = true; break; } } if (!found) { - distinctaddresses.push(value.address); - if (value.login) - distinctregusers++; + distinctaddresses.push(user.address); + } + if (user.login) { + regaddresses.push(user.address); + var found = false; + for (var i = 0; i < distinctregaddresses.length; i++) { + if (user.address == distinctregaddresses[i]) { + found = true; + break; + } + } + if (!found) { + distinctregaddresses.push(user.address); + } } }); User.getUserCount(function (err, regcount) { if (err) { - console.log('get status failed: ' + err); + logger.error('get status failed: ' + err); return; } if (callback) @@ -114,8 +127,8 @@ function getStatus(callback) { distinctOnlineUsers: distinctaddresses.length, notesCount: data.rows[0].count, registeredUsers: regcount, - onlineRegisteredUsers: regusers, - distinctOnlineRegisteredUsers: distinctregusers + onlineRegisteredUsers: regaddresses.length, + distinctOnlineRegisteredUsers: distinctregaddresses.length }); }); }); @@ -146,23 +159,40 @@ function emitOnlineUsers(socket) { if (user) users.push(buildUserOutData(user)); }); - notes[notename].socks.forEach(function (sock) { - var out = { - users: users - }; - out = LZString.compressToUTF16(JSON.stringify(out)); - sock.emit('online users', out); - }); + var out = { + users: users + }; + out = LZString.compressToUTF16(JSON.stringify(out)); + for (var i = 0, l = notes[notename].socks.length; i < l; i++) { + var sock = notes[notename].socks[i]; + if (sock && out) + sock.emit('online users', out); + }; } function emitUserStatus(socket) { var notename = getNotenameFromSocket(socket); if (!notename || !notes[notename]) return; - notes[notename].socks.forEach(function (sock) { + var out = buildUserOutData(users[socket.id]); + for (var i = 0, l = notes[notename].socks.length; i < l; i++) { + var sock = notes[notename].socks[i]; if (sock != socket) { - var out = buildUserOutData(users[socket.id]); sock.emit('user status', out); } + }; +} + +function emitRefresh(socket) { + var notename = getNotenameFromSocket(socket); + if (!notename || !notes[notename]) return; + var note = notes[notename]; + socket.emit('refresh', { + owner: note.owner, + permission: note.permission, + body: note.body, + otk: note.otk, + hash: note.hash, + updatetime: note.updatetime }); } @@ -175,9 +205,7 @@ function finishConnection(socket, notename) { notes[notename].users[socket.id] = users[socket.id]; notes[notename].socks.push(socket); emitOnlineUsers(socket); - socket.emit('refresh', { - body: notes[notename].body - }); + emitRefresh(socket); //clear finished socket in queue for (var i = 0; i < connectionSocketQueue.length; i++) { @@ -190,11 +218,11 @@ function finishConnection(socket, notename) { startConnection(connectionSocketQueue[0]); if (config.debug) { - console.log('SERVER connected a client to [' + notename + ']:'); - console.log(JSON.stringify(users[socket.id])); - //console.log(notes); + logger.info('SERVER connected a client to [' + notename + ']:'); + logger.info(JSON.stringify(users[socket.id])); + //logger.info(notes); getStatus(function (data) { - console.log(JSON.stringify(data)); + logger.info(JSON.stringify(data)); }); } } @@ -219,17 +247,34 @@ function startConnection(socket) { connectionSocketQueue.splice(i, 1); } isConnectionBusy = false; - return console.error(err); + return logger.error(err); } - var body = LZString.decompressFromBase64(data.rows[0].content); - body = LZString.compressToUTF16(body); - notes[notename] = { - socks: [], - body: body, - isDirty: false, - users: {} - }; - finishConnection(socket, notename); + var owner = data.rows[0].owner; + var permission = "freely"; + if (owner && owner != "null") { + permission = "editable"; + } + Note.findOrNewNote(notename, permission, function (err, note) { + if (err) { + responseError(res, "404", "Not Found", "oops."); + return; + } + var body = LZString.decompressFromBase64(data.rows[0].content); + body = LZString.compressToUTF16(body); + var updatetime = data.rows[0].update_time; + notes[notename] = { + owner: owner, + permission: note.permission, + socks: [], + body: body, + isDirty: false, + users: {}, + otk: shortId.generate(), + hash: md5(body), + updatetime: moment(updatetime).valueOf() + }; + finishConnection(socket, notename); + }); }); } else { finishConnection(socket, notename); @@ -241,8 +286,8 @@ function disconnect(socket) { isDisconnectBusy = true; if (config.debug) { - console.log("SERVER disconnected a client"); - console.log(JSON.stringify(users[socket.id])); + logger.info("SERVER disconnected a client"); + logger.info(JSON.stringify(users[socket.id])); } var notename = getNotenameFromSocket(socket); if (!notename) return; @@ -251,24 +296,31 @@ function disconnect(socket) { } if (notes[notename]) { delete notes[notename].users[socket.id]; - var index = notes[notename].socks.indexOf(socket); - if (index > -1) { - notes[notename].socks.splice(index, 1); - } + do { + var index = notes[notename].socks.indexOf(socket); + if (index != -1) { + notes[notename].socks.splice(index, 1); + } + } while (index != -1); if (Object.keys(notes[notename].users).length <= 0) { - var body = LZString.decompressFromUTF16(notes[notename].body); - var title = Note.getNoteTitle(body); - body = LZString.compressToBase64(body); - db.saveToDB(notename, title, body, - function (err, result) { - delete notes[notename]; - if (config.debug) { - //console.log(notes); - getStatus(function (data) { - console.log(JSON.stringify(data)); - }); - } - }); + if (notes[notename].isDirty) { + var body = LZString.decompressFromUTF16(notes[notename].body); + var title = Note.getNoteTitle(body); + title = LZString.compressToBase64(title); + body = LZString.compressToBase64(body); + db.saveToDB(notename, title, body, + function (err, result) { + delete notes[notename]; + if (config.debug) { + //logger.info(notes); + getStatus(function (data) { + logger.info(JSON.stringify(data)); + }); + } + }); + } else { + delete notes[notename]; + } } } emitOnlineUsers(socket); @@ -284,9 +336,9 @@ function disconnect(socket) { disconnect(disconnectSocketQueue[0]); if (config.debug) { - //console.log(notes); + //logger.info(notes); getStatus(function (data) { - console.log(JSON.stringify(data)); + logger.info(JSON.stringify(data)); }); } } @@ -309,6 +361,24 @@ function updateUserData(socket, user) { //retrieve user data from passport if (socket.request.user && socket.request.user.logged_in) { var profile = JSON.parse(socket.request.user.profile); + /* + var photo = null; + switch(profile.provider) { + case "facebook": + console.log(profile); + break; + case "twitter": + photo = profile.photos[0]; + break; + case "github": + photo = profile.avatar_url; + break; + case "dropbox": + //not image api provided + break; + } + user.photo = photo; + */ user.name = profile.displayName || profile.username; user.userid = socket.request.user._id; user.login = true; @@ -353,7 +423,6 @@ function connection(socket) { id: socket.id, address: socket.handshake.address, 'user-agent': socket.handshake.headers['user-agent'], - otk: shortId.generate(), color: color, cursor: null, login: false, @@ -368,24 +437,41 @@ function connection(socket) { connectionSocketQueue.push(socket); startConnection(socket); - //when a new client coming or received a client refresh request - socket.on('refresh', function (body_) { + //received client refresh request + socket.on('refresh', function () { + emitRefresh(socket); + }); + + //received client data updated + socket.on('update', function (body_) { var notename = getNotenameFromSocket(socket); - if (!notename) return; + if (!notename || !notes[notename]) return; if (config.debug) - console.log('SERVER received [' + notename + '] data updated: ' + socket.id); - if (notes[notename].body != body_) { - notes[notename].body = body_; - notes[notename].isDirty = true; + logger.info('SERVER received [' + notename + '] data updated: ' + socket.id); + var note = notes[notename]; + if (note.body != body_) { + note.body = body_; + note.hash = md5(body_); + note.updatetime = Date.now(); + note.isDirty = true; } + var out = { + id: socket.id, + hash: note.hash, + updatetime: note.updatetime + }; + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i]; + sock.emit('check', out); + }; }); //received user status socket.on('user status', function (data) { var notename = getNotenameFromSocket(socket); - if (!notename) return; + if (!notename || !notes[notename]) return; if (config.debug) - console.log('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data)); + logger.info('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data)); if (data) { var user = users[socket.id]; user.idle = data.idle; @@ -394,9 +480,40 @@ function connection(socket) { emitUserStatus(socket); }); + //received note permission change request + socket.on('permission', function (permission) { + //need login to do more actions + if (socket.request.user && socket.request.user.logged_in) { + var notename = getNotenameFromSocket(socket); + if (!notename || !notes[notename]) return; + var note = notes[notename]; + //Only owner can change permission + if (note.owner == socket.request.user._id) { + note.permission = permission; + Note.findNote(notename, function (err, _note) { + if (err || !_note) { + return; + } + Note.updatePermission(_note, permission, function (err, _note) { + if (err || !_note) { + return; + } + var out = { + permission: permission + }; + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i]; + sock.emit('permission', out); + }; + }); + }); + } + } + }); + //reveiced when user logout or changed socket.on('user changed', function () { - console.log('user changed'); + logger.info('user changed'); var notename = getNotenameFromSocket(socket); if (!notename || !notes[notename]) return; updateUserData(socket, notes[notename].users[socket.id]); @@ -431,11 +548,12 @@ function connection(socket) { if (!notename || !notes[notename]) return; users[socket.id].cursor = data; var out = buildUserOutData(users[socket.id]); - notes[notename].socks.forEach(function (sock) { + for (var i = 0, l = notes[notename].socks.length; i < l; i++) { + var sock = notes[notename].socks[i]; if (sock != socket) { sock.emit('cursor focus', out); } - }); + }; }); //received cursor activity @@ -444,11 +562,12 @@ function connection(socket) { if (!notename || !notes[notename]) return; users[socket.id].cursor = data; var out = buildUserOutData(users[socket.id]); - notes[notename].socks.forEach(function (sock) { + for (var i = 0, l = notes[notename].socks.length; i < l; i++) { + var sock = notes[notename].socks[i]; if (sock != socket) { sock.emit('cursor activity', out); } - }); + }; }); //received cursor blur @@ -459,13 +578,12 @@ function connection(socket) { var out = { id: socket.id }; - notes[notename].socks.forEach(function (sock) { + for (var i = 0, l = notes[notename].socks.length; i < l; i++) { + var sock = notes[notename].socks[i]; if (sock != socket) { - if (sock != socket) { - sock.emit('cursor blur', out); - } + sock.emit('cursor blur', out); } - }); + }; }); //when a new client disconnect @@ -477,12 +595,30 @@ function connection(socket) { //when received client change data request socket.on('change', function (op) { var notename = getNotenameFromSocket(socket); - if (!notename) return; + if (!notename || !notes[notename]) return; + var note = notes[notename]; + switch (note.permission) { + case "freely": + //not blocking anyone + break; + case "editable": + //only login user can change + if (!socket.request.user || !socket.request.user.logged_in) + return; + break; + case "locked": + //only owner can change + if (note.owner != socket.request.user._id) + return; + break; + } op = LZString.decompressFromUTF16(op); if (op) op = JSON.parse(op); + else + return; if (config.debug) - console.log('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op)); + logger.info('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op)); switch (op.origin) { case '+input': case '+delete': @@ -499,16 +635,20 @@ function connection(socket) { case '+joinLines': case '+duplicateLine': case '+sortLines': - notes[notename].socks.forEach(function (sock) { - if (sock != socket) { - if (config.debug) - console.log('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + JSON.stringify(op)); - sock.emit('change', LZString.compressToUTF16(JSON.stringify(op))); - } - }); + op.id = socket.id; + op.otk = note.otk; + op.nextotk = note.otk = shortId.generate(); + var stringop = JSON.stringify(op); + var compressstringop = LZString.compressToUTF16(stringop); + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i]; + if (config.debug) + logger.info('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + stringop); + sock.emit('change', compressstringop); + }; break; - default: - console.log('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op)); + default: + logger.info('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op)); } }); } diff --git a/lib/response.js b/lib/response.js index a7dcc0236..d3b011bf2 100644 --- a/lib/response.js +++ b/lib/response.js @@ -6,6 +6,8 @@ var path = require('path'); var uuid = require('node-uuid'); var markdownpdf = require("markdown-pdf"); var LZString = require('lz-string'); +var S = require('string'); +var shortId = require('shortid'); //core var config = require("../config.js"); @@ -31,16 +33,20 @@ var response = { newNote: newNote, showFeatures: showFeatures, showNote: showNote, - noteActions: noteActions + showShareNote: showShareNote, + noteActions: noteActions, + shareNoteActions: shareNoteActions }; function responseError(res, code, detail, msg) { res.writeHead(code, { 'Content-Type': 'text/html' }); - var content = ejs.render(fs.readFileSync(config.errorpath, 'utf8'), { + var template = config.errorpath; + var content = ejs.render(fs.readFileSync(template, 'utf8'), { + title: code + ' ' + detail + ' ' + msg, cache: !config.debug, - filename: config.errorpath, + filename: template, code: code, detail: detail, msg: msg @@ -49,16 +55,44 @@ function responseError(res, code, detail, msg) { res.end(); } -function responseHackMD(res) { - res.writeHead(200, { - 'Content-Type': 'text/html' +function responseHackMD(res, noteId) { + if (noteId != config.featuresnotename) { + if (!Note.checkNoteIdValid(noteId)) { + responseError(res, "404", "Not Found", "oops."); + return; + } + noteId = LZString.decompressFromBase64(noteId); + if (!noteId) { + responseError(res, "404", "Not Found", "oops."); + return; + } + } + db.readFromDB(noteId, function (err, data) { + if (err) { + responseError(res, "404", "Not Found", "oops."); + return; + } + var title = data.rows[0].title; + var decodedTitle = LZString.decompressFromBase64(title); + if (decodedTitle) title = decodedTitle; + title = Note.generateWebTitle(title); + var template = config.hackmdpath; + var options = { + cache: !config.debug, + filename: template + }; + var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options); + var html = compiled({ + title: title + }); + var buf = html; + res.writeHead(200, { + 'Content-Type': 'text/html; charset=UTF-8', + 'Cache-Control': 'private', + 'Content-Length': buf.length + }); + res.end(buf); }); - var content = ejs.render(fs.readFileSync(config.hackmdpath, 'utf8'), { - cache: !config.debug, - filename: config.hackmdpath - }); - res.write(content); - res.end(); } function newNote(req, res, next) { @@ -88,10 +122,10 @@ function showFeatures(req, res, next) { responseError(res, "500", "Internal Error", "wtf."); return; } - responseHackMD(res); + responseHackMD(res, config.featuresnotename); }); } else { - responseHackMD(res); + responseHackMD(res, config.featuresnotename); } }); } @@ -102,22 +136,105 @@ function showNote(req, res, next) { responseError(res, "404", "Not Found", "oops."); return; } - responseHackMD(res); + responseHackMD(res, noteId); } +function showShareNote(req, res, next) { + var shortid = req.params.shortid; + if (shortId.isValid(shortid)) { + Note.findNote(shortid, function (err, note) { + if (err || !note) { + responseError(res, "404", "Not Found", "oops."); + return; + } + //increase note viewcount + Note.increaseViewCount(note, function (err, note) { + if (err || !note) { + responseError(res, "404", "Not Found", "oops."); + return; + } + db.readFromDB(note.id, function (err, data) { + if (err) { + responseError(res, "404", "Not Found", "oops."); + return; + } + var body = LZString.decompressFromBase64(data.rows[0].content); + var updatetime = data.rows[0].update_time; + var text = S(body).escapeHTML().s; + var title = data.rows[0].title; + var decodedTitle = LZString.decompressFromBase64(title); + if (decodedTitle) title = decodedTitle; + title = Note.generateWebTitle(title); + var template = config.prettypath; + var options = { + cache: !config.debug, + filename: template + }; + var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options); + var origin = "//" + req.headers.host; + var html = compiled({ + title: title, + viewcount: note.viewcount, + updatetime: updatetime, + url: origin, + body: text + }); + var buf = html; + res.writeHead(200, { + 'Content-Type': 'text/html; charset=UTF-8', + 'Cache-Control': 'private', + 'Content-Length': buf.length + }); + res.end(buf); + }); + }); + }); + } else { + responseError(res, "404", "Not Found", "oops."); + } +} + +function actionShare(req, res, noteId) { + db.readFromDB(noteId, function (err, data) { + if (err) { + responseError(res, "404", "Not Found", "oops."); + return; + } + var owner = data.rows[0].owner; + var permission = "freely"; + if (owner && owner != "null") { + permission = "editable"; + } + Note.findOrNewNote(noteId, permission, function (err, note) { + if (err) { + responseError(res, "404", "Not Found", "oops."); + return; + } + res.redirect("/s/" + note.shortid); + }); + }); +} + +//pretty api is deprecated function actionPretty(req, res, noteId) { db.readFromDB(noteId, function (err, data) { if (err) { responseError(res, "404", "Not Found", "oops."); return; } - var body = data.rows[0].content; + var body = LZString.decompressFromBase64(data.rows[0].content); + var text = S(body).escapeHTML().s; + var title = data.rows[0].title; + var decodedTitle = LZString.decompressFromBase64(title); + if (decodedTitle) title = decodedTitle; + title = Note.generateWebTitle(title); var template = config.prettypath; var compiled = ejs.compile(fs.readFileSync(template, 'utf8')); var origin = "//" + req.headers.host; var html = compiled({ + title: title, url: origin, - body: body + body: text }); var buf = html; res.writeHead(200, { @@ -190,8 +307,9 @@ function noteActions(req, res, next) { } var action = req.params.action; switch (action) { - case "pretty": - actionPretty(req, res, noteId); + case "share": + case "pretty": //pretty deprecated + actionShare(req, res, noteId); break; case "download": actionDownload(req, res, noteId); @@ -208,4 +326,25 @@ function noteActions(req, res, next) { } } +function shareNoteActions(req, res, next) { + var action = req.params.action; + switch (action) { + case "edit": + var shortid = req.params.shortid; + if (shortId.isValid(shortid)) { + Note.findNote(shortid, function (err, note) { + if (err || !note) { + responseError(res, "404", "Not Found", "oops."); + return; + } + if (note.id != config.featuresnotename) + res.redirect('/' + LZString.compressToBase64(note.id)); + else + res.redirect('/' + note.id); + }); + } + break; + } +} + module.exports = response; \ No newline at end of file diff --git a/lib/temp.js b/lib/temp.js index 90d9343f2..b635644db 100644 --- a/lib/temp.js +++ b/lib/temp.js @@ -4,6 +4,7 @@ var mongoose = require('mongoose'); //core var config = require("../config.js"); +var logger = require("./logger.js"); // create a temp model var model = mongoose.model('temp', { @@ -33,13 +34,13 @@ function findTemp(id, callback) { id: id }, function (err, temp) { if (err) { - console.log('find temp failed: ' + err); + logger.error('find temp failed: ' + err); callback(err, null); } if (!err && temp) { callback(null, temp); } else { - console.log('find temp failed: ' + err); + logger.error('find temp failed: ' + err); callback(err, null); }; }); @@ -53,10 +54,10 @@ function newTemp(id, data, callback) { }); temp.save(function (err) { if (err) { - console.log('new temp failed: ' + err); + logger.error('new temp failed: ' + err); callback(err, null); } else { - console.log("new temp success: " + temp.id); + logger.info("new temp success: " + temp.id); callback(null, temp); }; }); @@ -67,14 +68,14 @@ function removeTemp(id, callback) { if(!err && temp) { temp.remove(function(err) { if(err) { - console.log('remove temp failed: ' + err); + logger.error('remove temp failed: ' + err); callback(err, null); } else { callback(null, null); } }); } else { - console.log('remove temp failed: ' + err); + logger.error('remove temp failed: ' + err); callback(err, null); } }); diff --git a/lib/user.js b/lib/user.js index 110645062..f89f7def4 100644 --- a/lib/user.js +++ b/lib/user.js @@ -4,6 +4,7 @@ var mongoose = require('mongoose'); //core var config = require("../config.js"); +var logger = require("./logger.js"); // create a user model var model = mongoose.model('user', { @@ -34,13 +35,13 @@ function findUser(id, callback) { id: id }, function (err, user) { if (err) { - console.log('find user failed: ' + err); + logger.error('find user failed: ' + err); callback(err, null); } if (!err && user) { callback(null, user); } else { - console.log('find user failed: ' + err); + logger.error('find user failed: ' + err); callback(err, null); }; }); @@ -54,10 +55,10 @@ function newUser(id, profile, callback) { }); user.save(function (err) { if (err) { - console.log('new user failed: ' + err); + logger.error('new user failed: ' + err); callback(err, null); } else { - console.log("new user success: " + user.id); + logger.info("new user success: " + user.id); callback(null, user); }; }); @@ -68,7 +69,7 @@ function findOrNewUser(id, profile, callback) { if(err || !user) { newUser(id, profile, function(err, user) { if(err) { - console.log('find or new user failed: ' + err); + logger.error('find or new user failed: ' + err); callback(err, null); } else { callback(null, user); diff --git a/logs/.keep b/logs/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json index a0f80b596..0b9fe41ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hackmd", - "version": "0.2.9", + "version": "0.3.1", "description": "Realtime collaborative markdown notes on all platforms.", "main": "app.js", "author": "jackycute", @@ -8,6 +8,7 @@ "license": "MIT", "dependencies": { "async": "^0.9.0", + "blueimp-md5": "^1.1.0", "body-parser": "^1.12.3", "chance": "^0.7.5", "cheerio": "^0.19.0", @@ -27,6 +28,7 @@ "markdown-pdf": "^5.2.0", "marked": "^0.3.3", "method-override": "^2.3.2", + "moment": "^2.10.3", "mongoose": "^4.0.2", "morgan": "^1.5.3", "node-uuid": "^1.4.3", @@ -38,8 +40,9 @@ "passport.socketio": "^3.5.1", "pg": "4.x", "randomcolor": "^0.2.0", - "shortid": "2.1.3", + "shortid": "2.2.2", "socket.io": "1.3.5", + "string": "^3.2.0", "toobusy-js": "^0.4.1", "winston": "^1.0.0" }, diff --git a/processes.json b/processes.json new file mode 100644 index 000000000..7f8740499 --- /dev/null +++ b/processes.json @@ -0,0 +1,19 @@ +{ + "apps": [{ + "name": "hackmd", + "script": "app.js", + "exec_mode": "fork", + "instances": 1, + "error_file": "./logs/hackmd-err.log", + "out_file": "./logs/hackmd-out.log", + "pid_file": "./hackmd.pid", + "env": { + "NODE_ENV": "production", + "DATABASE_URL": "change this", + "MONGOLAB_URI": "change this", + "PORT": "80", + "SSLPORT": "443", + "DOMAIN": "change this" + } + }] +} \ No newline at end of file diff --git a/public/css/cover.css b/public/css/cover.css index 30d54d342..b5d82e017 100644 --- a/public/css/cover.css +++ b/public/css/cover.css @@ -223,10 +223,21 @@ input { border-radius: 5px; color: black; text-shadow: none; + min-height: 134px; + display: table; + min-width: 100%; } -.list li .item .tags { +.list li .item .content { + display: table-cell; + vertical-align: middle; +} +.list li .item .content .tags { line-height: 25px; } +.list li .item .content .tags span { + display: inline-block; + line-height: 15px; +} .form-inline { padding: 0 10px; } @@ -246,6 +257,10 @@ input { .ui-history-close:hover { opacity: 1; } +.ui-or { + margin-top: 5px; + margin-bottom: 5px; +} .modal-title { text-align: left; diff --git a/public/css/extra.css b/public/css/extra.css index 8b7eb8846..6d9d137fb 100644 --- a/public/css/extra.css +++ b/public/css/extra.css @@ -13,15 +13,21 @@ } .vimeo .icon, .youtube .icon { - opacity: 0.5; + opacity: 0.3; display: table-cell; vertical-align: middle; height: inherit; margin: 0 auto; + color: white; + -webkit-transition: opacity 0.2s; /* Safari */ + transition: opacity 0.2s; + } .vimeo:hover .icon, .youtube:hover .icon { opacity: 0.6; + -webkit-transition: opacity 0.2s; /* Safari */ + transition: opacity 0.2s; } h1:hover .header-link, h2:hover .header-link, @@ -44,4 +50,168 @@ h6:hover .header-link { -moz-transition: opacity 0.2s ease-in-out 0.1s; -o-transition: opacity 0.2s ease-in-out 0.1s; transition: opacity 0.2s ease-in-out 0.1s; +} + +.ui-infobar { + max-width: 758px; + margin-top: 25px; + margin-bottom: -25px; + color: #777; +} + +.ui-toc { + position: fixed; + bottom: 20px; + z-index: 10000; +} + +.ui-toc-label { + opacity: 0.3; + background-color: #ccc; + border: none; + -webkit-transition: opacity 0.2s; /* Safari */ + transition: opacity 0.2s; +} + +.ui-toc .open .ui-toc-label { + opacity: 1; + color: white; + -webkit-transition: opacity 0.2s; /* Safari */ + transition: opacity 0.2s; +} + +.ui-toc-label:focus { + opacity: 0.3; + background-color: #ccc; + color: black; +} + +.ui-toc-label:hover { + opacity: 1; + background-color: #ccc; + -webkit-transition: opacity 0.2s; /* Safari */ + transition: opacity 0.2s; +} + +.ui-toc-dropdown { + margin-top: 20px; + margin-bottom: 20px; + padding-left: 10px; + padding-right: 10px; + max-width: 45vw; + width: 25vw; + max-height: 65vh; + overflow: auto; +} + +.ui-toc-dropdown a { + overflow: hidden; + text-overflow: ellipsis; + white-space: pre; +} + +.ui-toc-dropdown .nav>li>a { + display: block; + padding: 4px 20px; + font-size: 13px; + font-weight: 500; + color: #767676; +} + +.ui-toc-dropdown .nav>li>a:focus,.ui-toc-dropdown .nav>li>a:hover { + padding-left: 19px; + color: black; + text-decoration: none; + background-color: transparent; + border-left: 1px solid black; +} + +.ui-toc-dropdown .nav>.active:focus>a,.ui-toc-dropdown .nav>.active:hover>a,.ui-toc-dropdown .nav>.active>a { + padding-left: 18px; + font-weight: 700; + color: black; + background-color: transparent; + border-left: 2px solid black; +} + +.ui-toc-dropdown .nav .nav { + display: none; + padding-bottom: 10px; +} + +.ui-toc-dropdown .nav>.active>ul { + display: block; +} + +.ui-toc-dropdown .nav .nav>li>a { + padding-top: 1px; + padding-bottom: 1px; + padding-left: 30px; + font-size: 12px; + font-weight: 400; +} + +.ui-toc-dropdown .nav .nav>li>ul>li>a { + padding-top: 1px; + padding-bottom: 1px; + padding-left: 40px; + font-size: 12px; + font-weight: 400; +} + +.ui-toc-dropdown .nav .nav>li>a:focus,.ui-toc-dropdown .nav .nav>li>a:hover { + padding-left: 29px; +} + +.ui-toc-dropdown .nav .nav>li>ul>li>a:focus,.ui-toc-dropdown .nav .nav>li>ul>li>a:hover { + padding-left: 39px; +} + +.ui-toc-dropdown .nav .nav>.active:focus>a,.ui-toc-dropdown .nav .nav>.active:hover>a,.ui-toc-dropdown .nav .nav>.active>a { + padding-left: 28px; + font-weight: 500; +} + +.ui-toc-dropdown .nav .nav>.active>.nav>.active:focus>a,.ui-toc-dropdown .nav .nav>.active>.nav>.active:hover>a,.ui-toc-dropdown .nav .nav>.active>.nav>.active>a { + padding-left: 38px; + font-weight: 500; +} + +.ui-affix-toc { + position: fixed; + top: 0; + max-width: 15vw; + max-height: 70vh; + overflow: auto; +} + +.back-to-top, .go-to-bottom { + display: block; + padding: 4px 10px; + margin-top: 10px; + margin-left: 10px; + font-size: 12px; + font-weight: 500; + color: #999; +} + +.back-to-top:hover, .back-to-top:focus, .go-to-bottom:hover, .go-to-bottom:focus { + color: #563d7c; + text-decoration: none; +} + +.go-to-bottom { + margin-top: 0; +} + +small span { + line-height: 22px; +} + +small .dropdown { + display: inline-block; +} + +small .dropdown a:focus, small .dropdown a:hover { + text-decoration: none; } \ No newline at end of file diff --git a/public/css/index.css b/public/css/index.css index f1f303ed4..e1d5745f2 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -177,11 +177,11 @@ div[contenteditable]:empty:not(:focus):before{ content:attr(data-ph); color: gray; } -.dropdown-menu { +.dropdown-menu.list { max-height: 80vh; overflow: auto; } -.dropdown-menu::-webkit-scrollbar { +.dropdown-menu.list::-webkit-scrollbar { display: none; } .dropdown-menu .emoji { @@ -201,6 +201,26 @@ div[contenteditable]:empty:not(:focus):before{ user-select: none; } +.btn-file { + position: relative; + overflow: hidden; +} +.btn-file input[type=file] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + filter: alpha(opacity=0); + opacity: 0; + outline: none; + background: white; + cursor: inherit; + display: block; +} + .cm-trailing-space-a:before, .cm-trailing-space-b:before, .cm-trailing-space-new-line:before { diff --git a/public/css/site.css b/public/css/site.css index eed8b9501..29426b154 100644 --- a/public/css/site.css +++ b/public/css/site.css @@ -13,4 +13,10 @@ body { } ::-moz-focus-inner { border: 0 !important; +} + +/* manual fix for bootstrap issue 14040, there is an unnecessary padding-right on modal open */ +body.modal-open { + overflow-y: auto; + padding-right: 0 !important; } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 2eb76ddc4..c314ebd92 100644 --- a/public/index.html +++ b/public/index.html @@ -52,7 +52,7 @@

Realtime collaborative markdown notes on all platforms.

- +

Start new note @@ -66,7 +66,7 @@

+ + + + + + \ No newline at end of file diff --git a/public/views/foot.ejs b/public/views/foot.ejs index 8ef3cc219..0e808e6a0 100644 --- a/public/views/foot.ejs +++ b/public/views/foot.ejs @@ -1,5 +1,6 @@ + @@ -13,6 +14,7 @@ + @@ -29,6 +31,9 @@ + + + + + + diff --git a/run.sh b/run.sh index 07c146220..386ce4276 100644 --- a/run.sh +++ b/run.sh @@ -5,4 +5,4 @@ MONGOLAB_URI='change this' \ PORT='80' \ SSLPORT='443' \ DOMAIN='change this' \ -forever -a --uid hackmd start app.js \ No newline at end of file +forever -a --uid hackmd -l ./logs/hackmd_log.log -o ./logs/hackmd_out.log -e ./logs/hackmd_error.log start app.js \ No newline at end of file diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 000000000..e69de29bb