Refactor server with Sequelize ORM, refactor server configs, now will show note status (created or updated) and support docs (note alias)

This commit is contained in:
Cheng-Han, Wu 2016-04-20 18:03:55 +08:00
parent e613aeba75
commit 49b51e478f
35 changed files with 1877 additions and 2120 deletions

View file

@ -9,6 +9,17 @@ Still in early stage, feel free to fork or contribute to this.
Thanks for your using! :smile: Thanks for your using! :smile:
[docker-hackmd](https://github.com/hackmdio/docker-hackmd)
---
Before you going too far, here is the great docker repo for HackMD.
With docker, you can deploy a server in minutes without any hardtime.
[migration-to-0.4.0](https://github.com/hackmdio/migration-to-0.4.0)
---
We've dropped MongoDB after version 0.4.0.
So here is the migration tool for you to transfer old DB data to new DB.
This tool is also used for official service.
Browsers Requirement Browsers Requirement
--- ---
- Chrome >= 45, Chrome for Android >= 47 - Chrome >= 45, Chrome for Android >= 47
@ -20,33 +31,24 @@ Browsers Requirement
Prerequisite Prerequisite
--- ---
- Node.js 4.x or up (test up to 5.8.0) - Node.js 4.x or up (test up to 5.10.1)
- PostgreSQL 9.3.x or 9.4.x - Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL)
- MongoDB 3.0.x
- npm and bower - npm and bower
Get started Get started
--- ---
1. Download a release and unzip or clone into a directory 1. Download a release and unzip or clone into a directory
2. Enter the directory and type `npm install && bower install`, will install all the dependencies 2. Enter the directory and type `npm install && bower install`, will install all the dependencies
3. Install PostgreSQL and MongoDB (yes, currently we need both) 3. Setup the configs, see more on below
4. Import database schema, see more on below 4. Setup environment variables, which will overwrite the configs
5. Setup the configs, see more on below 5. Run the server as you like (node, forever, pm2)
6. Setup environment variables, which will overwrite the configs
7. Run the server as you like (node, forever, pm2)
Import database schema
---
The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql`
To import the sql file in PostgreSQL, see http://www.postgresql.org/docs/9.4/static/backup-dump.html
The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string.
Structure Structure
--- ---
``` ```
hackmd/ hackmd/
├── tmp/ --- temporary files ├── tmp/ --- temporary files
├── docs/ --- document files
├── lib/ --- server libraries ├── lib/ --- server libraries
└── public/ --- client files └── public/ --- client files
├── css/ --- css styles ├── css/ --- css styles
@ -57,63 +59,58 @@ hackmd/
Configuration files Configuration files
--- ---
There are some config you need to change in below files There are some configs you need to change in below files
``` ```
./config.js --- for server settings ./config.json --- for server settings
./public/js/index.js --- for client settings
./public/js/common.js --- for client settings ./public/js/common.js --- for client settings
``` ```
Client-side index.js settings Client settings `common.js`
--- ---
| variables | example values | description | | variables | example values | description |
| --------- | ------ | ----------- | | --------- | ------ | ----------- |
| debug | `true` or `false` | set debug mode, show more logs | | debug | `true` or `false` | set debug mode, show more logs |
| version | `0.3.2` | current version, must match same var in server side `config.js` |
Client-side common.js settings
---
| variables | example values | description |
| --------- | ------ | ----------- |
| domain | `localhost` | domain name | | domain | `localhost` | domain name |
| urlpath | `hackmd` | sub url path, like: `www.example.com/<urlpath>` | | urlpath | `hackmd` | sub url path, like: `www.example.com/<urlpath>` |
Environment variables Environment variables (will overwrite other server configs)
--- ---
| variables | example values | description | | variables | example values | description |
| --------- | ------ | ----------- | | --------- | ------ | ----------- |
| NODE_ENV | `production` or `development` | show current environment status | | NODE_ENV | `production` or `development` | set current environment (will apply correspond settings in the `config.json`) |
| DATABASE_URL | `postgresql://user:pass@host:port/hackmd` | PostgreSQL connection string | | PORT | `80` | web app port |
| MONGOLAB_URI | `mongodb://user:pass@host:port/hackmd` | MongoDB connection string | | DEBUG | `true` or `false` | set debug mode, show more logs |
| PORT | `80` | web port |
| SSLPORT | `443` | ssl web port |
| DOMAIN | `localhost` | domain name |
| URL_PATH | `hackmd` | sub url path, like `www.example.com/<URL_PATH>` |
Server-side config.js settings Server settings `config.json`
--- ---
| variables | example values | description | | variables | example values | description |
| --------- | ------ | ----------- | | --------- | ------ | ----------- |
| testport | `3000` | debug web port, fallback to this when not set in environment |
| testsslport | `3001` | debug web ssl port, fallback to this when not set in environment |
| usessl | `true` or `false` | set to use ssl |
| protocolusessl | `true` or `false` | set to use ssl protocol |
| urladdport | `true` or `false` | set to add port on oauth callback url |
| debug | `true` or `false` | set debug mode, show more logs | | debug | `true` or `false` | set debug mode, show more logs |
| usecdn | `true` or `false` | set to use CDN resources or not | | domain | `localhost` | domain name |
| version | `0.3.2` | currnet version, must match same var in client side `index.js` | | urlpath | `hackmd` | sub url path, like `www.example.com/<urlpath>` |
| port | `80` | web app port |
| alloworigin | `['localhost']` | domain name whitelist | | alloworigin | `['localhost']` | domain name whitelist |
| sslkeypath | `./cert/client.key` | ssl key path | | usessl | `true` or `false` | set to use ssl server (if true will auto turn on `protocolusessl`) |
| sslcertpath | `./cert/hackmd_io.crt` | ssl cert path | | protocolusessl | `true` or `false` | set to use ssl protocol for resources path |
| sslcapath | `['./cert/COMODORSAAddTrustCA.crt']` | ssl ca chain | | urladdport | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) |
| dhparampath | `./cert/dhparam.pem` | ssl dhparam path | | usecdn | `true` or `false` | set to use CDN resources or not |
| tmppath | `./tmp/` | temp file path | | db | `{ "dialect": "sqlite", "storage": "./db.hackmd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) |
| postgresqlstring | `postgresql://user:pass@host:port/hackmd` | PostgreSQL connection string, fallback to this when not set in environment | | sslkeypath | `./cert/client.key` | ssl key path (only need when you set usessl) |
| mongodbstring | `mongodb://user:pass@host:port/hackmd` | MongoDB connection string, fallback to this when not set in environment | | sslcertpath | `./cert/hackmd_io.crt` | ssl cert path (only need when you set usessl) |
| sslcapath | `['./cert/COMODORSAAddTrustCA.crt']` | ssl ca chain (only need when you set usessl) |
| dhparampath | `./cert/dhparam.pem` | ssl dhparam path (only need when you set usessl) |
| tmppath | `./tmp/` | temp directory path |
| defaultnotepath | `./public/default.md` | default note file path |
| docspath | `./public/docs` | docs directory path |
| indexpath | `./public/views/index.ejs` | index template file path |
| hackmdpath | `./public/views/hackmd.ejs` | hackmd template file path |
| errorpath | `./public/views/error.ejs` | error template file path |
| prettypath | `./public/views/pretty.ejs` | pretty template file path |
| slidepath | `./public/views/slide.hbs` | slide template file path |
| sessionname | `connect.sid` | cookie session name | | sessionname | `connect.sid` | cookie session name |
| sessionsecret | `secret` | cookie session secret | | sessionsecret | `secret` | cookie session secret |
| sessionlife | `14 * 24 * 60 * 60 * 1000` | cookie session life | | sessionlife | `14 * 24 * 60 * 60 * 1000` | cookie session life |
| sessiontouch | `1 * 3600` | cookie session touch | | staticcachetime | `1 * 24 * 60 * 60 * 1000` | static file cache time |
| heartbeatinterval | `5000` | socket.io heartbeat interval | | heartbeatinterval | `5000` | socket.io heartbeat interval |
| heartbeattimeout | `10000` | socket.io heartbeat timeout | | heartbeattimeout | `10000` | socket.io heartbeat timeout |
| documentmaxlength | `100000` | note max length | | documentmaxlength | `100000` | note max length |
@ -122,8 +119,8 @@ Third-party integration api key settings
--- ---
| service | file path | description | | service | file path | description |
| ------- | --------- | ----------- | | ------- | --------- | ----------- |
| facebook, twitter, github, dropbox | `config.js` | for signin | | facebook, twitter, github, dropbox | `config.json` | for signin |
| imgur | `config.js` | for image upload | | imgur | `config.json` | for image upload |
| dropbox | `public/views/foot.ejs` | for chooser and saver | | dropbox | `public/views/foot.ejs` | for chooser and saver |
| google drive | `public/js/common.js` | for export and import | | google drive | `public/js/common.js` | for export and import |

298
app.js
View file

@ -7,12 +7,10 @@ var passport = require('passport');
var methodOverride = require('method-override'); var methodOverride = require('method-override');
var cookieParser = require('cookie-parser'); var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser'); var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var compression = require('compression') var compression = require('compression')
var session = require('express-session'); var session = require('express-session');
var MongoStore = require('connect-mongo')(session); var SequelizeStore = require('connect-session-sequelize')(session.Store);
var fs = require('fs'); var fs = require('fs');
var shortid = require('shortid');
var imgur = require('imgur'); var imgur = require('imgur');
var formidable = require('formidable'); var formidable = require('formidable');
var morgan = require('morgan'); var morgan = require('morgan');
@ -20,12 +18,11 @@ var passportSocketIo = require("passport.socketio");
var helmet = require('helmet'); var helmet = require('helmet');
//core //core
var config = require("./config.js"); var config = require("./lib/config.js");
var logger = require("./lib/logger.js"); var logger = require("./lib/logger.js");
var User = require("./lib/user.js");
var Temp = require("./lib/temp.js");
var auth = require("./lib/auth.js"); var auth = require("./lib/auth.js");
var response = require("./lib/response.js"); var response = require("./lib/response.js");
var models = require("./lib/models");
//server setup //server setup
if (config.usessl) { if (config.usessl) {
@ -60,11 +57,7 @@ app.use(morgan('combined', {
//socket io //socket io
var io = require('socket.io')(server); var io = require('socket.io')(server);
// connect to the mongodb
mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring);
//others //others
var db = require("./lib/db.js");
var realtime = require("./lib/realtime.js"); var realtime = require("./lib/realtime.js");
//assign socket io to realtime //assign socket io to realtime
@ -82,13 +75,9 @@ var urlencodedParser = bodyParser.urlencoded({
}); });
//session store //session store
var sessionStore = new MongoStore({ var sessionStore = new SequelizeStore({
mongooseConnection: mongoose.connection, db: models.sequelize
touchAfter: config.sessiontouch });
},
function (err) {
logger.info(err);
});
//compression //compression
app.use(compression()); app.use(compression());
@ -139,15 +128,21 @@ app.use(passport.session());
//serialize and deserialize //serialize and deserialize
passport.serializeUser(function (user, done) { passport.serializeUser(function (user, done) {
//logger.info('serializeUser: ' + user._id); logger.info('serializeUser: ' + user.id);
done(null, user._id); return done(null, user.id);
}); });
passport.deserializeUser(function (id, done) { passport.deserializeUser(function (id, done) {
User.model.findById(id, function (err, user) { models.User.findOne({
//logger.info(user) where: {
if (!err) done(null, user); id: id
else done(err, null); }
}) }).then(function (user) {
logger.info('deserializeUser: ' + user.id);
return done(null, user);
}).catch(function (err) {
logger.error(err);
return done(err, null);
});
}); });
//routes //routes
@ -161,13 +156,17 @@ app.engine('html', ejs.renderFile);
//get index //get index
app.get("/", response.showIndex); app.get("/", response.showIndex);
//get 403 forbidden //get 403 forbidden
app.get("/403", function(req, res) { app.get("/403", function (req, res) {
response.errorForbidden(res); response.errorForbidden(res);
}); });
//get 404 not found //get 404 not found
app.get("/404", function(req, res) { app.get("/404", function (req, res) {
response.errorNotFound(res); response.errorNotFound(res);
}); });
//get 500 internal error
app.get("/500", function (req, res) {
response.errorInternalError(res);
});
//get status //get status
app.get("/status", function (req, res, next) { app.get("/status", function (req, res, next) {
realtime.getStatus(function (data) { realtime.getStatus(function (data) {
@ -184,19 +183,26 @@ app.get("/temp", function (req, res) {
if (!tempid) if (!tempid)
response.errorForbidden(res); response.errorForbidden(res);
else { else {
Temp.findTemp(tempid, function (err, temp) { models.Temp.findOne({
if (err || !temp) where: {
response.errorForbidden(res); id: tempid
}
}).then(function (temp) {
if (!temp)
response.errorNotFound(res);
else { else {
res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Origin", "*");
res.send({ res.send({
temp: temp.data temp: temp.data
}); });
temp.remove(function (err) { temp.destroy().catch(function (err) {
if (err) if (err)
logger.error('remove temp failed: ' + err); logger.error('remove temp failed: ' + err);
}); });
} }
}).catch(function (err) {
logger.error(err);
return response.errorInternalError(res);
}); });
} }
} }
@ -207,15 +213,16 @@ app.post("/temp", urlencodedParser, function (req, res) {
if (config.alloworigin.indexOf(host) == -1) if (config.alloworigin.indexOf(host) == -1)
response.errorForbidden(res); response.errorForbidden(res);
else { else {
var id = shortid.generate();
var data = req.body.data; var data = req.body.data;
if (!id || !data) if (!data)
response.errorForbidden(res); response.errorForbidden(res);
else { else {
if (config.debug) if (config.debug)
logger.info('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) { models.Temp.create({
if (!err && temp) { data: data
}).then(function (temp) {
if (temp) {
res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Origin", "*");
res.send({ res.send({
status: 'ok', status: 'ok',
@ -223,125 +230,149 @@ app.post("/temp", urlencodedParser, function (req, res) {
}); });
} else } else
response.errorInternalError(res); response.errorInternalError(res);
}).catch(function (err) {
logger.error(err);
return response.errorInternalError(res);
}); });
} }
} }
}); });
//facebook auth //facebook auth
app.get('/auth/facebook', if (config.facebook) {
passport.authenticate('facebook'), app.get('/auth/facebook',
function (req, res) {}); passport.authenticate('facebook'),
//facebook auth callback function (req, res) {});
app.get('/auth/facebook/callback', //facebook auth callback
passport.authenticate('facebook', { app.get('/auth/facebook/callback',
failureRedirect: config.getserverurl() passport.authenticate('facebook', {
}), failureRedirect: config.serverurl
function (req, res) { }),
res.redirect(config.getserverurl()); function (req, res) {
}); res.redirect(config.serverurl);
});
}
//twitter auth //twitter auth
app.get('/auth/twitter', if (config.twitter) {
passport.authenticate('twitter'), app.get('/auth/twitter',
function (req, res) {}); passport.authenticate('twitter'),
//twitter auth callback function (req, res) {});
app.get('/auth/twitter/callback', //twitter auth callback
passport.authenticate('twitter', { app.get('/auth/twitter/callback',
failureRedirect: config.getserverurl() passport.authenticate('twitter', {
}), failureRedirect: config.serverurl
function (req, res) { }),
res.redirect(config.getserverurl()); function (req, res) {
}); res.redirect(config.serverurl);
});
}
//github auth //github auth
app.get('/auth/github', if (config.github) {
passport.authenticate('github'), app.get('/auth/github',
function (req, res) {}); passport.authenticate('github'),
//github auth callback function (req, res) {});
app.get('/auth/github/callback', //github auth callback
passport.authenticate('github', { app.get('/auth/github/callback',
failureRedirect: config.getserverurl() passport.authenticate('github', {
}), failureRedirect: config.serverurl
function (req, res) { }),
res.redirect(config.getserverurl()); function (req, res) {
}); res.redirect(config.serverurl);
//github callback actions });
app.get('/auth/github/callback/:noteId/:action', response.githubActions); //github callback actions
app.get('/auth/github/callback/:noteId/:action', response.githubActions);
}
//dropbox auth //dropbox auth
app.get('/auth/dropbox', if (config.dropbox) {
passport.authenticate('dropbox-oauth2'), app.get('/auth/dropbox',
function (req, res) {}); passport.authenticate('dropbox-oauth2'),
//dropbox auth callback function (req, res) {});
app.get('/auth/dropbox/callback', //dropbox auth callback
passport.authenticate('dropbox-oauth2', { app.get('/auth/dropbox/callback',
failureRedirect: config.getserverurl() passport.authenticate('dropbox-oauth2', {
}), failureRedirect: config.serverurl
function (req, res) { }),
res.redirect(config.getserverurl()); function (req, res) {
}); res.redirect(config.serverurl);
});
}
//logout //logout
app.get('/logout', function (req, res) { app.get('/logout', function (req, res) {
if (config.debug && req.isAuthenticated()) if (config.debug && req.isAuthenticated())
logger.info('user logout: ' + req.user._id); logger.info('user logout: ' + req.user.id);
req.logout(); req.logout();
res.redirect(config.getserverurl()); res.redirect(config.serverurl);
}); });
//get history //get history
app.get('/history', function (req, res) { app.get('/history', function (req, res) {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
User.model.findById(req.user._id, function (err, user) { models.User.findOne({
if (err) { where: {
logger.error('read history failed: ' + err); id: req.user.id
} else {
var history = [];
if (user.history)
history = JSON.parse(user.history);
res.send({
history: history
});
} }
}).then(function (user) {
if (!user)
return response.errorNotFound(res);
var history = [];
if (user.history)
history = JSON.parse(user.history);
res.send({
history: history
});
if (config.debug)
logger.info('read history success: ' + user.id);
}).catch(function (err) {
logger.error('read history failed: ' + err);
return response.errorInternalError(res);
}); });
} else { } else {
response.errorForbidden(res); return response.errorForbidden(res);
} }
}); });
//post history //post history
app.post('/history', urlencodedParser, function (req, res) { app.post('/history', urlencodedParser, function (req, res) {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
if (config.debug) if (config.debug)
logger.info('SERVER received history from [' + req.user._id + ']: ' + req.body.history); logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history);
User.model.findById(req.user._id, function (err, user) { models.User.update({
if (err) { history: req.body.history
logger.error('write history failed: ' + err); }, {
} else { where: {
user.history = req.body.history; id: req.user.id
user.save(function (err) {
if (err) {
logger.error('write user history failed: ' + err);
} else {
if (config.debug)
logger.info("write user history success: " + user._id);
};
});
} }
}).then(function (count) {
if (!count)
return response.errorNotFound(res);
if (config.debug)
logger.info("write user history success: " + req.user.id);
}).catch(function (err) {
logger.error('write history failed: ' + err);
return response.errorInternalError(res);
}); });
res.end(); res.end();
} else { } else {
response.errorForbidden(res); return response.errorForbidden(res);
} }
}); });
//get me info //get me info
app.get('/me', function (req, res) { app.get('/me', function (req, res) {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
User.model.findById(req.user._id, function (err, user) { models.User.findOne({
if (err) { where: {
logger.error('read me failed: ' + err); id: req.user.id
} else {
var profile = JSON.parse(user.profile);
res.send({
status: 'ok',
id: req.user._id,
name: profile.displayName || profile.username
});
} }
}).then(function (user) {
if (!user)
return response.errorNotFound(res);
var profile = models.User.parseProfile(user.profile);
res.send({
status: 'ok',
id: req.user.id,
name: profile.name,
photo: profile.photo
});
}).catch(function (err) {
logger.error('read me failed: ' + err);
return response.errorInternalError(res);
}); });
} else { } else {
res.send({ res.send({
@ -370,19 +401,17 @@ app.post('/uploadimage', function (req, res) {
}) })
.catch(function (err) { .catch(function (err) {
logger.error(err); logger.error(err);
res.send('upload image error'); return res.send('upload image error');
}); });
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
res.send('upload image error'); return res.send('upload image error');
} }
} }
}); });
}); });
//get new note //get new note
app.get("/new", response.newNote); app.get("/new", response.newNote);
//get features
app.get("/features", response.showFeatures);
//get publish note //get publish note
app.get("/s/:shortid", response.showPublishNote); app.get("/s/:shortid", response.showPublishNote);
//publish note actions //publish note actions
@ -412,15 +441,22 @@ io.set('heartbeat timeout', config.heartbeattimeout);
io.sockets.on('connection', realtime.connection); io.sockets.on('connection', realtime.connection);
//listen //listen
if (config.usessl) { function startListen() {
server.listen(config.sslport, function () { if (config.usessl) {
logger.info('HTTPS Server listening at sslport %d', config.sslport); server.listen(config.port, function () {
}); logger.info('HTTPS Server listening at port %d', config.port);
} else { });
server.listen(config.port, function () { } else {
logger.info('HTTP Server listening at port %d', config.port); server.listen(config.port, function () {
}); logger.info('HTTP Server listening at port %d', config.port);
});
}
} }
// sync db then start listen
models.sequelize.sync().then(startListen);
// log uncaught exception
process.on('uncaughtException', function (err) { process.on('uncaughtException', function (err) {
logger.error(err); logger.error(err);
}); });

View file

@ -1,88 +0,0 @@
//config
var path = require('path');
var domain = process.env.DOMAIN;
var urlpath = process.env.URL_PATH;
var testport = '3000';
var testsslport = '3001';
var port = process.env.PORT || testport;
var sslport = process.env.SSLPORT || testsslport;
var usessl = false; // use node https server
var protocolusessl = false; // use ssl protocol
var urladdport = true; //add port on getserverurl
var config = {
debug: false,
usecdn: false,
version: '0.3.4',
domain: domain,
alloworigin: ['add here to allow origin to cross'],
urlpath: urlpath,
testport: testport,
testsslport: testsslport,
port: port,
sslport: sslport,
sslkeypath: 'change this',
sslcertpath: 'change this',
sslcapath: ['change this'],
dhparampath: 'change this',
usessl: usessl,
protocolusessl: protocolusessl,
getserverurl: function() {
var protocol = protocolusessl ? 'https://' : 'http://';
var url = domain;
if (usessl)
url = protocol + url + (sslport == 443 || !urladdport ? '' : ':' + sslport);
else
url = protocol + url + (port == 80 || !urladdport ? '' : ':' + port);
if (urlpath)
url = url + '/' + urlpath;
return url;
},
//path
tmppath: "./tmp/",
defaultnotepath: path.join(__dirname, '/public', "default.md"),
defaultfeaturespath: path.join(__dirname, '/public', "features.md"),
indexpath: path.join(__dirname, '/public/', "index.ejs"),
hackmdpath: path.join(__dirname, '/public/views', "index.ejs"),
errorpath: path.join(__dirname, '/public/views', "error.ejs"),
prettypath: path.join(__dirname, '/public/views', 'pretty.ejs'),
//db string
postgresqlstring: "change this",
mongodbstring: "change this",
//constants
featuresnotename: "features",
sessionname: 'change this',
sessionsecret: 'change this',
sessionlife: 14 * 24 * 60 * 60 * 1000, //14 days
sessiontouch: 1 * 3600, //1 hour
heartbeatinterval: 5000,
heartbeattimeout: 10000,
documentmaxlength: 100000,
//auth
facebook: {
clientID: 'change this',
clientSecret: 'change this',
callbackPath: '/auth/facebook/callback'
},
twitter: {
consumerKey: 'change this',
consumerSecret: 'change this',
callbackPath: '/auth/twitter/callback'
},
github: {
clientID: 'change this',
clientSecret: 'change this',
callbackPath: '/auth/github/callback'
},
dropbox: {
clientID: 'change this',
clientSecret: 'change this',
callbackPath: '/auth/dropbox/callback'
},
imgur: {
clientID: 'change this'
}
};
module.exports = config;

43
config.json Normal file
View file

@ -0,0 +1,43 @@
{
"development": {
"domain": "localhost",
"db": {
"username": "",
"password": "",
"database": "hackmd",
"host": "localhost",
"port": "3306",
"dialect": "mysql"
}
},
"production": {
"domain": "localhost",
"db": {
"username": "",
"password": "",
"database": "hackmd",
"host": "localhost",
"port": "5432",
"dialect": "postgres"
},
"facebook": {
"clientID": "change this",
"clientSecret": "change this"
},
"twitter": {
"consumerKey": "change this",
"consumerSecret": "change this"
},
"github": {
"clientID": "change this",
"clientSecret": "change this"
},
"dropbox": {
"clientID": "change this",
"clientSecret": "change this"
},
"imgur": {
"clientID": "change this"
}
}
}

View file

@ -1,77 +0,0 @@
--
-- PostgreSQL database dump
--
SET statement_timeout = 0;
SET lock_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
--
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
--
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
--
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
SET search_path = public, pg_catalog;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- Name: notes; Type: TABLE; Schema: public; Owner: postgres; Tablespace:
--
CREATE TABLE notes (
id character varying(256) NOT NULL,
owner character varying(256) NOT NULL,
content text,
title text,
create_time timestamp without time zone DEFAULT now() NOT NULL,
update_time timestamp without time zone DEFAULT now() NOT NULL
);
ALTER TABLE notes OWNER TO "postgres";
--
-- Name: notes_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:
--
ALTER TABLE ONLY notes
ADD CONSTRAINT notes_pkey PRIMARY KEY (id);
--
-- Name: unique_notes; Type: CONSTRAINT; Schema: public; Owner: postgres; Tablespace:
--
ALTER TABLE ONLY notes
ADD CONSTRAINT unique_notes UNIQUE (id);
--
-- Name: public; Type: ACL; Schema: -; Owner: postgres
--
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON SCHEMA public FROM "postgres";
GRANT ALL ON SCHEMA public TO "postgres";
GRANT ALL ON SCHEMA public TO PUBLIC;
--
-- PostgreSQL database dump complete
--

View file

@ -7,44 +7,60 @@ var GithubStrategy = require('passport-github').Strategy;
var DropboxStrategy = require('passport-dropbox-oauth2').Strategy; var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
//core //core
var User = require('./user.js'); var config = require('./config.js');
var config = require('../config.js');
var logger = require("./logger.js"); var logger = require("./logger.js");
var models = require("./models");
function callback(accessToken, refreshToken, profile, done) { function callback(accessToken, refreshToken, profile, done) {
//logger.info(profile.displayName || profile.username); //logger.info(profile.displayName || profile.username);
User.findOrNewUser(profile.id, profile, function (err, user) { models.User.findOrCreate({
if (err || user == null) { where: {
logger.error('auth callback failed: ' + err); profileid: profile.id.toString()
} else { },
if (config.debug && user) defaults: {
logger.info('user login: ' + user._id); profile: JSON.stringify(profile)
done(null, user);
} }
}); }).spread(function(user, created) {
if (user) {
if (config.debug)
logger.info('user login: ' + user.id);
return done(null, user);
}
}).catch(function(err) {
logger.error('auth callback failed: ' + err);
return done(err, null);
})
} }
//facebook //facebook
module.exports = passport.use(new FacebookStrategy({ if (config.facebook) {
clientID: config.facebook.clientID, module.exports = passport.use(new FacebookStrategy({
clientSecret: config.facebook.clientSecret, clientID: config.facebook.clientID,
callbackURL: config.getserverurl() + config.facebook.callbackPath clientSecret: config.facebook.clientSecret,
}, callback)); callbackURL: config.serverurl + '/auth/facebook/callback'
}, callback));
}
//twitter //twitter
passport.use(new TwitterStrategy({ if (config.twitter) {
consumerKey: config.twitter.consumerKey, passport.use(new TwitterStrategy({
consumerSecret: config.twitter.consumerSecret, consumerKey: config.twitter.consumerKey,
callbackURL: config.getserverurl() + config.twitter.callbackPath consumerSecret: config.twitter.consumerSecret,
}, callback)); callbackURL: config.serverurl + '/auth/twitter/callback'
}, callback));
}
//github //github
passport.use(new GithubStrategy({ if (config.github) {
clientID: config.github.clientID, passport.use(new GithubStrategy({
clientSecret: config.github.clientSecret, clientID: config.github.clientID,
callbackURL: config.getserverurl() + config.github.callbackPath clientSecret: config.github.clientSecret,
}, callback)); callbackURL: config.serverurl + '/auth/github/callback'
}, callback));
}
//dropbox //dropbox
passport.use(new DropboxStrategy({ if (config.dropbox) {
clientID: config.dropbox.clientID, passport.use(new DropboxStrategy({
clientSecret: config.dropbox.clientSecret, clientID: config.dropbox.clientID,
callbackURL: config.getserverurl() + config.dropbox.callbackPath clientSecret: config.dropbox.clientSecret,
}, callback)); callbackURL: config.serverurl + '/auth/dropbox/callback'
}, callback));
}

112
lib/config.js Normal file
View file

@ -0,0 +1,112 @@
// external modules
var path = require('path');
// configs
var env = process.env.NODE_ENV || 'development';
var config = require(path.join(__dirname, '..', 'config.json'))[env];
var debug = process.env.DEBUG ? (process.env.DEBUG === 'true') : ((typeof config.debug === 'boolean') ? config.debug : (env === 'development'));
// url
var domain = config.domain || 'localhost';
var urlpath = config.urlpath || '';
var port = process.env.PORT || config.port || 3000;
var alloworigin = config.alloworigin || ['localhost'];
var usessl = !!config.usessl;
var protocolusessl = (config.usessl === true && typeof config.protocolusessl === 'undefined') ? true : !!config.protocolusessl;
var urladdport = !!config.urladdport;
var usecdn = !!config.usecdn;
// db
var db = config.db || {
dialect: 'sqlite',
storage: './db.hackmd.sqlite'
};
// ssl path
var sslkeypath = config.sslkeypath || ''
var sslcertpath = config.sslcertpath || '';
var sslcapath = config.sslcapath || '';
var dhparampath = config.dhparampath || '';
// other path
var tmppath = config.tmppath || './tmp';
var defaultnotepath = config.defaultnotepath || './public/default.md';
var docspath = config.docspath || './public/docs';
var indexpath = config.indexpath || './public/views/index.ejs';
var hackmdpath = config.hackmdpath || './public/views/hackmd.ejs';
var errorpath = config.errorpath || './public/views/error.ejs';
var prettypath = config.prettypath || './public/views/pretty.ejs';
var slidepath = config.slidepath || './public/views/slide.hbs';
// session
var sessionname = config.sessionname || 'connect.sid';
var sessionsecret = config.sessionsecret || 'secret';
var sessionlife = config.sessionlife || 14 * 24 * 60 * 60 * 1000; //14 days
// static files
var staticcachetime = config.staticcachetime || 1 * 24 * 60 * 60 * 1000; // 1 day
// socket.io
var heartbeatinterval = config.heartbeatinterval || 5000;
var heartbeattimeout = config.heartbeattimeout || 10000;
// document
var documentmaxlength = config.documentmaxlength || 100000;
// auth
var facebook = config.facebook || false;
var twitter = config.twitter || false;
var github = config.github || false;
var dropbox = config.dropbox || false;
var imgur = config.imgur || false;
function getserverurl() {
var protocol = protocolusessl ? 'https://' : 'http://';
var url = protocol + domain;
if (urladdport && ((usessl && port != 443) || (!usessl && port != 80)))
url += ':' + port;
if (urlpath)
url += '/' + urlpath;
return url;
}
var version = '0.4.0';
var cwd = path.join(__dirname, '..');
module.exports = {
version: version,
debug: debug,
urlpath: urlpath,
port: port,
alloworigin: alloworigin,
usessl: usessl,
serverurl: getserverurl(),
usecdn: usecdn,
db: db,
sslkeypath: path.join(cwd, sslkeypath),
sslcertpath: path.join(cwd, sslcertpath),
sslcapath: path.join(cwd, sslcapath),
dhparampath: path.join(cwd, dhparampath),
tmppath: path.join(cwd, tmppath),
defaultnotepath: path.join(cwd, defaultnotepath),
docspath: path.join(cwd, docspath),
indexpath: path.join(cwd, indexpath),
hackmdpath: path.join(cwd, hackmdpath),
errorpath: path.join(cwd, errorpath),
prettypath: path.join(cwd, prettypath),
slidepath: path.join(cwd, slidepath),
sessionname: sessionname,
sessionsecret: sessionsecret,
sessionlife: sessionlife,
staticcachetime: staticcachetime,
heartbeatinterval: heartbeatinterval,
heartbeattimeout: heartbeattimeout,
documentmaxlength: documentmaxlength,
facebook: facebook,
twitter: twitter,
github: github,
dropbox: dropbox,
imgur: imgur
};

151
lib/db.js
View file

@ -1,151 +0,0 @@
//db
//external modules
var pg = require('pg');
var fs = require('fs');
var util = require('util');
//core
var config = require("../config.js");
var logger = require("./logger.js");
//public
var db = {
readFromFile: readFromDB,
saveToFile: saveToFile,
newToDB: newToDB,
readFromDB: readFromDB,
saveToDB: saveToDB,
countFromDB: countFromDB
};
function getDBClient() {
return new pg.Client(process.env.DATABASE_URL || config.postgresqlstring);
}
function readFromFile(callback) {
fs.readFile('hackmd', 'utf8', function (err, data) {
if (err) throw err;
callback(data);
});
}
function saveToFile(doc) {
fs.writeFile('hackmd', doc, function (err) {
if (err) throw err;
});
}
var updatequery = "UPDATE notes SET title='%s', content='%s', update_time=NOW() WHERE id='%s';";
var insertquery = "INSERT INTO notes (id, owner, content) VALUES ('%s', '%s', '%s');";
var insertifnotexistquery = "INSERT INTO notes (id, owner, content) \
SELECT '%s', '%s', '%s' \
WHERE NOT EXISTS (SELECT 1 FROM notes WHERE id='%s') RETURNING *;";
var selectquery = "SELECT * FROM notes WHERE id='%s';";
var countquery = "SELECT count(*) FROM notes;";
function newToDB(id, owner, body, callback) {
var client = getDBClient();
client.connect(function (err) {
if (err) {
client.end();
callback(err, null);
return logger.error('could not connect to postgres', err);
}
var newnotequery = util.format(insertquery, id, owner, body);
//logger.info(newnotequery);
client.query(newnotequery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return logger.error("new note to db failed: " + err);
} else {
if (config.debug)
logger.info("new note to db success");
callback(null, result);
}
});
});
}
function readFromDB(id, callback) {
var client = getDBClient();
client.connect(function (err) {
if (err) {
client.end();
callback(err, null);
return logger.error('could not connect to postgres', err);
}
var readquery = util.format(selectquery, id);
//logger.info(readquery);
client.query(readquery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return logger.error("read from db failed: " + err);
} else {
//logger.info(result.rows);
if (result.rows.length <= 0) {
callback("not found note in db", null);
return logger.error("not found note in db: " + id, err);
} else {
if(config.debug)
logger.info("read from db success");
callback(null, result);
}
}
});
});
}
function saveToDB(id, title, data, callback) {
var client = getDBClient();
client.connect(function (err) {
if (err) {
client.end();
callback(err, null);
return logger.error('could not connect to postgres', err);
}
var savequery = util.format(updatequery, title, data, id);
//logger.info(savequery);
client.query(savequery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return logger.error("save to db failed: " + err);
} else {
if (config.debug)
logger.info("save to db success");
callback(null, result);
}
});
});
}
function countFromDB(callback) {
var client = getDBClient();
client.connect(function (err) {
if (err) {
client.end();
callback(err, null);
return logger.error('could not connect to postgres', err);
}
client.query(countquery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return logger.error("count from db failed: " + err);
} else {
//logger.info(result.rows);
if (result.rows.length <= 0) {
callback("not found note in db", null);
} else {
if(config.debug)
logger.info("count from db success");
callback(null, result);
}
}
});
});
}
module.exports = db;

37
lib/models/index.js Normal file
View file

@ -0,0 +1,37 @@
"use strict";
// external modules
var fs = require("fs");
var path = require("path");
var Sequelize = require("sequelize");
// core
var config = require('../config.js');
var logger = require("../logger.js");
var dbconfig = config.db;
dbconfig.logging = config.debug ? logger.info : false;
var sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig);
var db = {};
fs
.readdirSync(__dirname)
.filter(function (file) {
return (file.indexOf(".") !== 0) && (file !== "index.js");
})
.forEach(function (file) {
var model = sequelize.import(path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(function (modelName) {
if ("associate" in db[modelName]) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

208
lib/models/note.js Normal file
View file

@ -0,0 +1,208 @@
"use strict";
// external modules
var fs = require('fs');
var path = require('path');
var LZString = require('lz-string');
var marked = require('marked');
var cheerio = require('cheerio');
var shortId = require('shortid');
var Sequelize = require("sequelize");
var async = require('async');
// core
var config = require("../config.js");
var logger = require("../logger.js");
// permission types
var permissionTypes = ["freely", "editable", "locked", "private"];
module.exports = function (sequelize, DataTypes) {
var Note = sequelize.define("Note", {
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: Sequelize.UUIDV4
},
shortid: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
defaultValue: shortId.generate
},
alias: {
type: DataTypes.STRING,
unique: true
},
permission: {
type: DataTypes.ENUM,
values: permissionTypes
},
viewcount: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
title: {
type: DataTypes.TEXT
},
content: {
type: DataTypes.TEXT
},
lastchangeAt: {
type: DataTypes.DATE
}
}, {
classMethods: {
associate: function (models) {
Note.belongsTo(models.User, {
foreignKey: "ownerId",
as: "owner",
constraints: false
});
Note.belongsTo(models.User, {
foreignKey: "lastchangeuserId",
as: "lastchangeuser",
constraints: false
});
},
checkFileExist: function (filePath) {
try {
return fs.statSync(filePath).isFile();
} catch (err) {
return false;
}
},
checkNoteIdValid: function (id) {
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;
var result = id.match(uuidRegex);
if (result && result.length == 1)
return true;
else
return false;
},
parseNoteId: function (noteId, callback) {
async.series({
parseNoteIdByAlias: function (_callback) {
// try to parse note id by alias (e.g. doc)
Note.findOne({
where: {
alias: noteId
}
}).then(function (note) {
if (note) {
return callback(null, note.id);
} else {
var filePath = path.join(config.docspath, noteId + '.md');
if (Note.checkFileExist(filePath)) {
Note.create({
alias: noteId,
owner: null,
permission: 'locked'
}).then(function (note) {
return callback(null, note.id);
}).catch(function (err) {
return _callback(err, null);
});
} else {
return _callback(null, null);
}
}
}).catch(function (err) {
return _callback(err, null);
});
},
parseNoteIdByLZString: function (_callback) {
// try to parse note id by LZString Base64
try {
var id = LZString.decompressFromBase64(noteId);
if (id && Note.checkNoteIdValid(id))
return callback(null, id);
else
return _callback(null, null);
} catch (err) {
return _callback(err, null);
}
},
parseNoteIdByShortId: function (_callback) {
// try to parse note id by shortId
try {
if (shortId.isValid(noteId)) {
Note.findOne({
where: {
shortid: noteId
}
}).then(function (note) {
if (!note) return _callback(null, null);
return callback(null, note.id);
}).catch(function (err) {
return _callback(err, null);
});
} else {
return _callback(null, null);
}
} catch (err) {
return _callback(err, null);
}
}
}, function (err, result) {
if (err) {
logger.error(err);
return callback(err, null);
}
return callback(null, null);
});
},
parseNoteTitle: function (body) {
var $ = cheerio.load(marked(body));
var h1s = $("h1");
var title = "";
if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
title = h1s.first().text();
else
title = "Untitled";
return title;
},
decodeTitle: function (title) {
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
else title = 'Untitled';
return title;
},
generateWebTitle: function (title) {
title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD";
return title;
}
},
hooks: {
beforeCreate: function (note, options, callback) {
// if no content specified then use default note
if (!note.content) {
var body = null;
var filePath = null;
if (!note.alias) {
filePath = config.defaultnotepath;
} else {
filePath = path.join(config.docspath, note.alias + '.md');
}
if (Note.checkFileExist(filePath)) {
body = fs.readFileSync(filePath, 'utf8');
note.title = LZString.compressToBase64(Note.parseNoteTitle(body));
note.content = LZString.compressToBase64(body);
}
}
// if no permission specified and have owner then give editable permission, else default permission is freely
if (!note.permission) {
if (note.ownerId) {
note.permission = "editable";
} else {
note.permission = "freely";
}
}
return callback(null, note);
}
}
});
return Note;
};

19
lib/models/temp.js Normal file
View file

@ -0,0 +1,19 @@
"use strict";
//external modules
var shortId = require('shortid');
module.exports = function (sequelize, DataTypes) {
var Temp = sequelize.define("Temp", {
id: {
type: DataTypes.STRING,
primaryKey: true,
defaultValue: shortId.generate
},
data: {
type: DataTypes.TEXT
}
});
return Temp;
};

77
lib/models/user.js Normal file
View file

@ -0,0 +1,77 @@
"use strict";
// external modules
var md5 = require("blueimp-md5");
var Sequelize = require("sequelize");
// core
var logger = require("../logger.js");
module.exports = function (sequelize, DataTypes) {
var User = sequelize.define("User", {
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: Sequelize.UUIDV4
},
profileid: {
type: DataTypes.STRING,
unique: true
},
profile: {
type: DataTypes.TEXT
},
history: {
type: DataTypes.TEXT
}
}, {
classMethods: {
associate: function (models) {
User.hasMany(models.Note, {
foreignKey: "ownerId",
constraints: false
});
User.hasMany(models.Note, {
foreignKey: "lastchangeuserId",
constraints: false
});
},
parseProfile: function (profile) {
try {
var profile = JSON.parse(profile);
} catch (err) {
logger.error(err);
profile = null;
}
if (profile) {
profile = {
name: profile.displayName || profile.username,
photo: User.parsePhotoByProfile(profile)
}
}
return profile;
},
parsePhotoByProfile: function (profile) {
var photo = null;
switch (profile.provider) {
case "facebook":
photo = 'https://graph.facebook.com/' + profile.id + '/picture';
break;
case "twitter":
photo = profile.photos[0].value;
break;
case "github":
photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
break;
case "dropbox":
//no image api provided, use gravatar
photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
break;
}
return photo;
}
}
});
return User;
};

View file

@ -1,237 +0,0 @@
//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", "private"];
// create a note model
var model = mongoose.model('note', {
id: String,
shortid: {
type: String,
unique: true,
default: shortId.generate
},
permission: {
type: String,
enum: permissionTypes
},
lastchangeuser: {
type: Schema.Types.ObjectId,
ref: 'user'
},
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,
decodeTitle: decodeTitle,
generateWebTitle: generateWebTitle,
increaseViewCount: increaseViewCount,
updatePermission: updatePermission,
updateLastChangeUser: updateLastChangeUser
};
function checkNoteIdValid(noteId) {
try {
//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;
var result = id.match(uuidRegex);
if (result && result.length == 1)
return true;
else
return false;
} catch (err) {
logger.error(err);
return false;
}
}
function checkNoteExist(noteId) {
try {
//logger.info(noteId);
var id = LZString.decompressFromBase64(noteId);
db.readFromDB(id, function (err, result) {
if (err) return false;
return true;
});
} catch (err) {
logger.error(err);
return false;
}
}
//get title
function getNoteTitle(body) {
var $ = cheerio.load(marked(body));
var h1s = $("h1");
var title = "";
if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
title = h1s.first().text();
else
title = "Untitled";
return title;
}
// decode title
function decodeTitle(title) {
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
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, owner, callback) {
var permission = "freely";
if (owner && owner != "null") {
permission = "editable";
}
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, owner, callback) {
findNote(id, function (err, note) {
if (err || !note) {
newNote(id, owner, function (err, note) {
if (err) {
logger.error('find or new note failed: ' + err);
callback(err, null);
} else {
callback(null, note);
}
});
} else {
if (!note.permission) {
var permission = "freely";
if (owner && owner != "null") {
permission = "editable";
}
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);
};
});
}
function updateLastChangeUser(note, lastchangeuser, callback) {
note.lastchangeuser = lastchangeuser;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('update note lastchangeuser failed: ' + err);
callback(err, null);
} else {
logger.info("update note lastchangeuser success: " + note.id);
callback(null, note);
};
});
}
module.exports = note;

View file

@ -1,4 +1,4 @@
var config = require('../../config'); var config = require('../config');
if (typeof ot === 'undefined') { if (typeof ot === 'undefined') {
var ot = {}; var ot = {};

View file

@ -5,24 +5,19 @@ var cookieParser = require('cookie-parser');
var url = require('url'); var url = require('url');
var async = require('async'); var async = require('async');
var LZString = require('lz-string'); var LZString = require('lz-string');
var shortId = require('shortid');
var randomcolor = require("randomcolor"); var randomcolor = require("randomcolor");
var Chance = require('chance'), var Chance = require('chance'),
chance = new Chance(); chance = new Chance();
var moment = require('moment'); var moment = require('moment');
//core //core
var config = require("../config.js"); var config = require("./config.js");
var logger = require("./logger.js"); var logger = require("./logger.js");
var models = require("./models");
//ot //ot
var ot = require("./ot/index.js"); var ot = require("./ot/index.js");
//others
var db = require("./db.js");
var Note = require("./note.js");
var User = require("./user.js");
//public //public
var realtime = { var realtime = {
io: null, io: null,
@ -72,12 +67,6 @@ function emitCheck(note) {
lastchangeuserprofile: note.lastchangeuserprofile lastchangeuserprofile: note.lastchangeuserprofile
}; };
realtime.io.to(note.id).emit('check', out); realtime.io.to(note.id).emit('check', out);
/*
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
sock.emit('check', out);
};
*/
} }
//actions //actions
@ -88,70 +77,82 @@ var updater = setInterval(function () {
async.each(Object.keys(notes), function (key, callback) { async.each(Object.keys(notes), function (key, callback) {
var note = notes[key]; var note = notes[key];
if (note.server.isDirty) { if (note.server.isDirty) {
if (config.debug) if (config.debug) logger.info("updater found dirty note: " + key);
logger.info("updater found dirty note: " + key); updateNote(note, function(err, _note) {
updaterUpdateMongo(note, function(err, result) { if (!_note) {
if (err) return callback(err, null); realtime.io.to(note.id).emit('info', {
updaterUpdatePostgres(note, function(err, result) { code: 404
if (err) return callback(err, null); });
callback(null, null); logger.error('note not found: ', note.id);
}); }
if (err || !_note) {
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
sock.disconnect(true);
}
return callback(err, null);
}
note.server.isDirty = false;
note.updatetime = moment(_note.lastchangeAt).valueOf();
emitCheck(note);
return callback(null, null);
}); });
} else { } else {
callback(null, null); return callback(null, null);
} }
}, function (err) { }, function (err) {
if (err) return logger.error('updater error', err); if (err) return logger.error('updater error', err);
}); });
}, 1000); }, 1000);
function updaterUpdateMongo(note, callback) { function updateNote(note, callback) {
Note.findNote(note.id, function (err, _note) { models.Note.findOne({
if (err || !_note) return callback(err, null); where: {
id: note.id
}
}).then(function (_note) {
if (!_note) return callback(null, null);
if (note.lastchangeuser) { if (note.lastchangeuser) {
if (_note.lastchangeuser != note.lastchangeuser) { if (_note.lastchangeuserId != note.lastchangeuser) {
var lastchangeuser = note.lastchangeuser; models.User.findOne({
var lastchangeuserprofile = null; where: {
User.findUser(lastchangeuser, function (err, user) { id: note.lastchangeuser
if (err) return callback(err, null);
if (user && user.profile) {
var profile = JSON.parse(user.profile);
if (profile) {
lastchangeuserprofile = {
name: profile.displayName || profile.username,
photo: User.parsePhotoByProfile(profile)
}
_note.lastchangeuser = lastchangeuser;
note.lastchangeuserprofile = lastchangeuserprofile;
Note.updateLastChangeUser(_note, lastchangeuser, function (err, result) {
if (err) return callback(err, null);
callback(null, null);
});
}
} }
}).then(function (user) {
if (!user) return callback(null, null);
note.lastchangeuserprofile = models.User.parseProfile(user.profile);
return finishUpdateNote(note, _note, callback);
}).catch(function (err) {
logger.error(err);
return callback(err, null);
}); });
} else {
return finishUpdateNote(note, _note, callback);
} }
} else { } else {
_note.lastchangeuser = null;
note.lastchangeuserprofile = null; note.lastchangeuserprofile = null;
Note.updateLastChangeUser(_note, null, function (err, result) { return finishUpdateNote(note, _note, callback);
if (err) return callback(err, null);
callback(null, null);
});
} }
}).catch(function (err) {
logger.error(err);
return callback(err, null);
}); });
} }
function updaterUpdatePostgres(note, callback) { function finishUpdateNote(note, _note, callback) {
//postgres update
var body = note.server.document; var body = note.server.document;
var title = Note.getNoteTitle(body); var title = models.Note.parseNoteTitle(body);
title = LZString.compressToBase64(title); title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body); body = LZString.compressToBase64(body);
db.saveToDB(note.id, title, body, function (err, result) { var values = {
if (err) return callback(err, null); title: title,
note.server.isDirty = false; content: body,
note.updatetime = Date.now(); lastchangeuserId: note.lastchangeuser,
emitCheck(note); lastchangeAt: Date.now()
callback(null, null); };
_note.update(values).then(function (_note) {
return callback(null, _note);
}).catch(function (err) {
logger.error(err);
return callback(err, null);
}); });
} }
//clean when user not in any rooms or user not in connected list //clean when user not in any rooms or user not in connected list
@ -170,15 +171,14 @@ var cleaner = setInterval(function () {
disconnectSocketQueue.push(socket); disconnectSocketQueue.push(socket);
disconnect(socket); disconnect(socket);
} }
callback(null, null); return callback(null, null);
}, function (err) { }, function (err) {
if (err) return logger.error('cleaner error', err); if (err) return logger.error('cleaner error', err);
}); });
}, 60000); }, 60000);
function getStatus(callback) { function getStatus(callback) {
db.countFromDB(function (err, data) { models.Note.count().then(function (notecount) {
if (err) return logger.info(err);
var distinctaddresses = []; var distinctaddresses = [];
var regaddresses = []; var regaddresses = [];
var distinctregaddresses = []; var distinctregaddresses = [];
@ -208,58 +208,58 @@ function getStatus(callback) {
} }
} }
}); });
User.getUserCount(function (err, regcount) { models.User.count().then(function (regcount) {
if (err) { return callback ? callback({
logger.error('get status failed: ' + err); onlineNotes: Object.keys(notes).length,
return; onlineUsers: Object.keys(users).length,
} distinctOnlineUsers: distinctaddresses.length,
if (callback) notesCount: notecount,
callback({ registeredUsers: regcount,
onlineNotes: Object.keys(notes).length, onlineRegisteredUsers: regaddresses.length,
onlineUsers: Object.keys(users).length, distinctOnlineRegisteredUsers: distinctregaddresses.length,
distinctOnlineUsers: distinctaddresses.length, isConnectionBusy: isConnectionBusy,
notesCount: data.rows[0].count, connectionSocketQueueLength: connectionSocketQueue.length,
registeredUsers: regcount, isDisconnectBusy: isDisconnectBusy,
onlineRegisteredUsers: regaddresses.length, disconnectSocketQueueLength: disconnectSocketQueue.length
distinctOnlineRegisteredUsers: distinctregaddresses.length, }) : null;
isConnectionBusy: isConnectionBusy, }).catch(function (err) {
connectionSocketQueueLength: connectionSocketQueue.length, return logger.error('count user failed: ' + err);
isDisconnectBusy: isDisconnectBusy,
disconnectSocketQueueLength: disconnectSocketQueue.length
});
}); });
}).catch(function (err) {
return logger.error('count note failed: ' + err);
}); });
} }
function getNotenameFromSocket(socket) { function extractNoteIdFromSocket(socket) {
if (!socket || !socket.handshake || !socket.handshake.headers) { if (!socket || !socket.handshake || !socket.handshake.headers) {
return; return false;
} }
var referer = socket.handshake.headers.referer; var referer = socket.handshake.headers.referer;
if (!referer) { if (!referer) {
return socket.disconnect(true); return false;
} }
var hostUrl = url.parse(referer); var hostUrl = url.parse(referer);
var notename = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1]; var noteId = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1];
if (notename == config.featuresnotename) { return noteId;
return notename; }
function parseNoteIdFromSocket(socket, callback) {
var noteId = extractNoteIdFromSocket(socket);
if (!noteId) {
return callback(null, null);
} }
if (!Note.checkNoteIdValid(notename)) { models.Note.parseNoteId(noteId, function (err, id) {
socket.emit('info', { if (err || !id) return callback(err, id);
code: 404 return callback(null, id);
}); });
return socket.disconnect(true);
}
notename = LZString.decompressFromBase64(notename);
return notename;
} }
function emitOnlineUsers(socket) { function emitOnlineUsers(socket) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
var users = []; var users = [];
Object.keys(notes[notename].users).forEach(function (key) { Object.keys(notes[noteId].users).forEach(function (key) {
var user = notes[notename].users[key]; var user = notes[noteId].users[key];
if (user) if (user)
users.push(buildUserOutData(user)); users.push(buildUserOutData(user));
}); });
@ -267,35 +267,20 @@ function emitOnlineUsers(socket) {
users: users users: users
}; };
out = LZString.compressToUTF16(JSON.stringify(out)); out = LZString.compressToUTF16(JSON.stringify(out));
realtime.io.to(notename).emit('online users', out); realtime.io.to(noteId).emit('online users', 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) { function emitUserStatus(socket) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
var out = buildUserOutData(users[socket.id]); var out = buildUserOutData(users[socket.id]);
socket.broadcast.to(notename).emit('user status', out); socket.broadcast.to(noteId).emit('user status', out);
/*
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
sock.emit('user status', out);
}
};
*/
} }
function emitRefresh(socket) { function emitRefresh(socket) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
var note = notes[notename]; var note = notes[noteId];
socket.emit('refresh', { socket.emit('refresh', {
docmaxlength: config.documentmaxlength, docmaxlength: config.documentmaxlength,
owner: note.owner, owner: note.owner,
@ -326,15 +311,10 @@ function finishConnection(socket, note, user) {
if (!socket || !note || !user) return; if (!socket || !note || !user) return;
//check view permission //check view permission
if (note.permission == 'private') { if (note.permission == 'private') {
if (socket.request.user && socket.request.user.logged_in && socket.request.user._id == note.owner) { if (socket.request.user && socket.request.user.logged_in && socket.request.user.id == note.owner) {
//na //na
} else { } else {
socket.emit('info', { return failConnection(403, 'connection forbidden', socket);
code: 403
});
clearSocketQueue(connectionSocketQueue, socket);
isConnectionBusy = false;
return socket.disconnect(true);
} }
} }
note.users[socket.id] = user; note.users[socket.id] = user;
@ -354,8 +334,8 @@ function finishConnection(socket, note, user) {
startConnection(connectionSocketQueue[0]); startConnection(connectionSocketQueue[0]);
if (config.debug) { if (config.debug) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
logger.info('SERVER connected a client to [' + notename + ']:'); logger.info('SERVER connected a client to [' + noteId + ']:');
logger.info(JSON.stringify(user)); logger.info(JSON.stringify(user));
//logger.info(notes); //logger.info(notes);
getStatus(function (data) { getStatus(function (data) {
@ -367,117 +347,76 @@ function finishConnection(socket, note, user) {
function startConnection(socket) { function startConnection(socket) {
if (isConnectionBusy) return; if (isConnectionBusy) return;
isConnectionBusy = true; isConnectionBusy = true;
var noteId = socket.noteId;
if (!noteId) {
return failConnection(404, 'note id not found', socket);
}
var notename = getNotenameFromSocket(socket); if (!notes[noteId]) {
if (!notename) { var include = [{
clearSocketQueue(connectionSocketQueue, socket); model: models.User,
isConnectionBusy = false; as: "owner"
return; }, {
} model: models.User,
as: "lastchangeuser"
}];
if (!notes[notename]) { models.Note.findOne({
db.readFromDB(notename, function (err, data) { where: {
if (err) { id: noteId
socket.emit('info', { },
code: 404 include: include
}); }).then(function (note) {
socket.disconnect(true); if (!note) {
//clear err socket in queue return failConnection(404, 'note not found', socket);
clearSocketQueue(connectionSocketQueue, socket);
isConnectionBusy = false;
return logger.error(err);
} }
var owner = note.ownerId;
var ownerprofile = note.owner ? models.User.parseProfile(note.owner.profile) : null;
var owner = data.rows[0].owner; var lastchangeuser = note.lastchangeuserId;
var ownerprofile = null; var lastchangeuserprofile = note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null;
//find or new note var body = LZString.decompressFromBase64(note.content);
Note.findOrNewNote(notename, owner, function (err, note) { var createtime = note.createdAt;
if (err) { var updatetime = note.lastchangeAt;
socket.emit('info', { var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit);
code: 404
});
socket.disconnect(true);
clearSocketQueue(connectionSocketQueue, socket);
isConnectionBusy = false;
return logger.error(err);
}
var body = LZString.decompressFromBase64(data.rows[0].content); notes[noteId] = {
//body = LZString.compressToUTF16(body); id: noteId,
var createtime = data.rows[0].create_time; owner: owner,
var updatetime = data.rows[0].update_time; ownerprofile: ownerprofile,
var server = new ot.EditorSocketIOServer(body, [], notename, ifMayEdit); permission: note.permission,
lastchangeuser: lastchangeuser,
lastchangeuserprofile: lastchangeuserprofile,
socks: [],
users: {},
createtime: moment(createtime).valueOf(),
updatetime: moment(updatetime).valueOf(),
server: server
};
var lastchangeuser = note.lastchangeuser || null; return finishConnection(socket, notes[noteId], users[socket.id]);
var lastchangeuserprofile = null; }).catch(function (err) {
return failConnection(500, err, socket);
notes[notename] = {
id: notename,
owner: owner,
ownerprofile: ownerprofile,
permission: note.permission,
lastchangeuser: lastchangeuser,
lastchangeuserprofile: lastchangeuserprofile,
socks: [],
users: {},
createtime: moment(createtime).valueOf(),
updatetime: moment(updatetime).valueOf(),
server: server
};
async.parallel([
function getlastchangeuser(callback) {
if (lastchangeuser) {
//find last change user profile if lastchangeuser exists
User.findUser(lastchangeuser, function (err, user) {
if (!err && user && user.profile) {
var profile = JSON.parse(user.profile);
if (profile) {
lastchangeuserprofile = {
name: profile.displayName || profile.username,
photo: User.parsePhotoByProfile(profile)
}
notes[notename].lastchangeuserprofile = lastchangeuserprofile;
}
}
callback(null, null);
});
} else {
callback(null, null);
}
},
function getowner(callback) {
if (owner && owner != "null") {
//find owner profile if owner exists
User.findUser(owner, function (err, user) {
if (!err && user && user.profile) {
var profile = JSON.parse(user.profile);
if (profile) {
ownerprofile = {
name: profile.displayName || profile.username,
photo: User.parsePhotoByProfile(profile)
}
notes[notename].ownerprofile = ownerprofile;
}
}
callback(null, null);
});
} else {
callback(null, null);
}
}
], function(err, results){
if (err) return;
finishConnection(socket, notes[notename], users[socket.id]);
});
});
}); });
} else { } else {
finishConnection(socket, notes[notename], users[socket.id]); return finishConnection(socket, notes[noteId], users[socket.id]);
} }
} }
function failConnection(code, err, socket) {
logger.error(err);
// clear error socket in queue
clearSocketQueue(connectionSocketQueue, socket);
isConnectionBusy = false;
// emit error info
socket.emit('info', {
code: code
});
return socket.disconnect(true);
}
function disconnect(socket) { function disconnect(socket) {
if (isDisconnectBusy) return; if (isDisconnectBusy) return;
isDisconnectBusy = true; isDisconnectBusy = true;
@ -490,8 +429,8 @@ function disconnect(socket) {
if (users[socket.id]) { if (users[socket.id]) {
delete users[socket.id]; delete users[socket.id];
} }
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
var note = notes[notename]; var note = notes[noteId];
if (note) { if (note) {
delete note.users[socket.id]; delete note.users[socket.id];
do { do {
@ -502,22 +441,18 @@ function disconnect(socket) {
} while (index != -1); } while (index != -1);
if (Object.keys(note.users).length <= 0) { if (Object.keys(note.users).length <= 0) {
if (note.server.isDirty) { if (note.server.isDirty) {
var body = note.server.document; updateNote(note, function (err, _note) {
var title = Note.getNoteTitle(body); if (err) return logger.error('disconnect note failed: ' + err);
title = LZString.compressToBase64(title); delete notes[noteId];
body = LZString.compressToBase64(body); if (config.debug) {
db.saveToDB(notename, title, body, //logger.info(notes);
function (err, result) { getStatus(function (data) {
delete notes[notename]; logger.info(JSON.stringify(data));
if (config.debug) { });
//logger.info(notes); }
getStatus(function (data) { });
logger.info(JSON.stringify(data));
});
}
});
} else { } else {
delete notes[notename]; delete notes[noteId];
} }
} }
} }
@ -556,10 +491,10 @@ function buildUserOutData(user) {
function updateUserData(socket, user) { function updateUserData(socket, user) {
//retrieve user data from passport //retrieve user data from passport
if (socket.request.user && socket.request.user.logged_in) { if (socket.request.user && socket.request.user.logged_in) {
var profile = JSON.parse(socket.request.user.profile); var profile = models.User.parseProfile(socket.request.user.profile);
user.photo = User.parsePhotoByProfile(profile); user.photo = profile.photo;
user.name = profile.displayName || profile.username; user.name = profile.name;
user.userid = socket.request.user._id; user.userid = socket.request.user.id;
user.login = true; user.login = true;
} else { } else {
user.userid = null; user.userid = null;
@ -569,9 +504,9 @@ function updateUserData(socket, user) {
} }
function ifMayEdit(socket, callback) { function ifMayEdit(socket, callback) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
var note = notes[notename]; var note = notes[noteId];
var mayEdit = true; var mayEdit = true;
switch (note.permission) { switch (note.permission) {
case "freely": case "freely":
@ -584,69 +519,78 @@ function ifMayEdit(socket, callback) {
break; break;
case "locked": case "private": case "locked": case "private":
//only owner can change //only owner can change
if (note.owner != socket.request.user._id) if (note.owner != socket.request.user.id)
mayEdit = false; mayEdit = false;
break; break;
} }
//if user may edit and this note have owner (not anonymous usage) //if user may edit and this note have owner (not anonymous usage)
if (socket.origin == 'operation' && mayEdit && note.owner && note.owner != "null") { if (socket.origin == 'operation' && mayEdit && note.owner) {
//save for the last change user id //save for the last change user id
if (socket.request.user && socket.request.user.logged_in) { if (socket.request.user && socket.request.user.logged_in) {
note.lastchangeuser = socket.request.user._id; note.lastchangeuser = socket.request.user.id;
} else { } else {
note.lastchangeuser = null; note.lastchangeuser = null;
} }
} }
callback(mayEdit); return callback(mayEdit);
} }
function connection(socket) { function connection(socket) {
//split notename from socket parseNoteIdFromSocket(socket, function (err, noteId) {
var notename = getNotenameFromSocket(socket); if (err) {
return failConnection(500, err, socket);
//initialize user data }
//random color if (!noteId) {
var color = randomcolor({ return failConnection(404, 'note id not found', socket);
luminosity: 'light' }
});
//make sure color not duplicated or reach max random count // store noteId in this socket session
if (notename && notes[notename]) { socket.noteId = noteId;
var randomcount = 0;
var maxrandomcount = 5; //initialize user data
var found = false; //random color
do { var color = randomcolor({
Object.keys(notes[notename].users).forEach(function (user) { luminosity: 'light'
if (user.color == color) { });
found = true; //make sure color not duplicated or reach max random count
return; if (notes[noteId]) {
} var randomcount = 0;
}); var maxrandomcount = 5;
if (found) { var found = false;
color = randomcolor({ do {
luminosity: 'light' Object.keys(notes[noteId].users).forEach(function (user) {
if (user.color == color) {
found = true;
return;
}
}); });
randomcount++; if (found) {
} color = randomcolor({
} while (found && randomcount < maxrandomcount); luminosity: 'light'
} });
//create user data randomcount++;
users[socket.id] = { }
id: socket.id, } while (found && randomcount < maxrandomcount);
address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address, }
'user-agent': socket.handshake.headers['user-agent'], //create user data
color: color, users[socket.id] = {
cursor: null, id: socket.id,
login: false, address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address,
userid: null, 'user-agent': socket.handshake.headers['user-agent'],
name: null, color: color,
idle: false, cursor: null,
type: null login: false,
}; userid: null,
updateUserData(socket, users[socket.id]); name: null,
idle: false,
type: null
};
updateUserData(socket, users[socket.id]);
//start connection //start connection
connectionSocketQueue.push(socket); connectionSocketQueue.push(socket);
startConnection(socket); startConnection(socket);
});
//received client refresh request //received client refresh request
socket.on('refresh', function () { socket.on('refresh', function () {
@ -655,10 +599,10 @@ function connection(socket) {
//received user status //received user status
socket.on('user status', function (data) { socket.on('user status', function (data) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
if (config.debug) if (config.debug)
logger.info('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data)); logger.info('SERVER received [' + noteId + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
if (data) { if (data) {
var user = users[socket.id]; var user = users[socket.id];
user.idle = data.idle; user.idle = data.idle;
@ -671,41 +615,44 @@ function connection(socket) {
socket.on('permission', function (permission) { socket.on('permission', function (permission) {
//need login to do more actions //need login to do more actions
if (socket.request.user && socket.request.user.logged_in) { if (socket.request.user && socket.request.user.logged_in) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
var note = notes[notename]; var note = notes[noteId];
//Only owner can change permission //Only owner can change permission
if (note.owner == socket.request.user._id) { if (note.owner == socket.request.user.id) {
note.permission = permission; note.permission = permission;
Note.findNote(notename, function (err, _note) { models.Note.update({
if (err || !_note) { permission: permission
}, {
where: {
id: noteId
}
}).then(function (count) {
if (!count) {
return; return;
} }
Note.updatePermission(_note, permission, function (err, _note) { var out = {
if (err || !_note) { permission: permission
return; };
} realtime.io.to(note.id).emit('permission', out);
var out = { for (var i = 0, l = note.socks.length; i < l; i++) {
permission: permission var sock = note.socks[i];
}; if (typeof sock !== 'undefined' && sock) {
realtime.io.to(note.id).emit('permission', out); //check view permission
for (var i = 0, l = note.socks.length; i < l; i++) { if (permission == 'private') {
var sock = note.socks[i]; if (sock.request.user && sock.request.user.logged_in && sock.request.user.id == note.owner) {
if (typeof sock !== 'undefined' && sock) { //na
//check view permission } else {
if (permission == 'private') { sock.emit('info', {
if (sock.request.user && sock.request.user.logged_in && sock.request.user._id == note.owner) { code: 403
//na });
} else { return sock.disconnect(true);
sock.emit('info', {
code: 403
});
return sock.disconnect(true);
}
} }
} }
} }
}); }
}).catch(function (err) {
return logger.error('update note permission failed: ' + err);
}); });
} }
} }
@ -714,19 +661,19 @@ function connection(socket) {
//reveiced when user logout or changed //reveiced when user logout or changed
socket.on('user changed', function () { socket.on('user changed', function () {
logger.info('user changed'); logger.info('user changed');
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
updateUserData(socket, notes[notename].users[socket.id]); updateUserData(socket, notes[noteId].users[socket.id]);
emitOnlineUsers(socket); emitOnlineUsers(socket);
}); });
//received sync of online users request //received sync of online users request
socket.on('online users', function () { socket.on('online users', function () {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
var users = []; var users = [];
Object.keys(notes[notename].users).forEach(function (key) { Object.keys(notes[noteId].users).forEach(function (key) {
var user = notes[notename].users[key]; var user = notes[noteId].users[key];
if (user) if (user)
users.push(buildUserOutData(user)); users.push(buildUserOutData(user));
}); });
@ -744,55 +691,31 @@ function connection(socket) {
//received cursor focus //received cursor focus
socket.on('cursor focus', function (data) { socket.on('cursor focus', function (data) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
users[socket.id].cursor = data; users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]); var out = buildUserOutData(users[socket.id]);
socket.broadcast.to(notename).emit('cursor focus', out); socket.broadcast.to(noteId).emit('cursor focus', out);
/*
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 //received cursor activity
socket.on('cursor activity', function (data) { socket.on('cursor activity', function (data) {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
users[socket.id].cursor = data; users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]); var out = buildUserOutData(users[socket.id]);
socket.broadcast.to(notename).emit('cursor activity', out); socket.broadcast.to(noteId).emit('cursor activity', out);
/*
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 //received cursor blur
socket.on('cursor blur', function () { socket.on('cursor blur', function () {
var notename = getNotenameFromSocket(socket); var noteId = socket.noteId;
if (!notename || !notes[notename]) return; if (!noteId || !notes[noteId]) return;
users[socket.id].cursor = null; users[socket.id].cursor = null;
var out = { var out = {
id: socket.id id: socket.id
}; };
socket.broadcast.to(notename).emit('cursor blur', out); socket.broadcast.to(noteId).emit('cursor blur', out);
/*
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
sock.emit('cursor blur', out);
}
};
*/
}); });
//when a new client disconnect //when a new client disconnect

View file

@ -3,7 +3,6 @@
var ejs = require('ejs'); var ejs = require('ejs');
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var uuid = require('node-uuid');
var markdownpdf = require("markdown-pdf"); var markdownpdf = require("markdown-pdf");
var LZString = require('lz-string'); var LZString = require('lz-string');
var S = require('string'); var S = require('string');
@ -13,12 +12,9 @@ var querystring = require('querystring');
var request = require('request'); var request = require('request');
//core //core
var config = require("../config.js"); var config = require("./config.js");
var logger = require("./logger.js");
//others var models = require("./models");
var db = require("./db.js");
var Note = require("./note.js");
var User = require("./user.js");
//slides //slides
var md = require('reveal.js/plugin/markdown/markdown'); var md = require('reveal.js/plugin/markdown/markdown');
@ -26,10 +22,7 @@ var Mustache = require('mustache');
//reveal.js //reveal.js
var opts = { var opts = {
userBasePath: process.cwd(), template: fs.readFileSync(config.slidepath).toString(),
revealBasePath: path.resolve(require.resolve('reveal.js'), '..', '..'),
template: fs.readFileSync(path.join('.', '/public/views/slide', 'reveal.hbs')).toString(),
templateListing: fs.readFileSync(path.join('.', '/public/views/slide', 'listing.hbs')).toString(),
theme: 'css/theme/black.css', theme: 'css/theme/black.css',
highlightTheme: 'zenburn', highlightTheme: 'zenburn',
separator: '^(\r\n?|\n)---(\r\n?|\n)$', separator: '^(\r\n?|\n)---(\r\n?|\n)$',
@ -52,7 +45,6 @@ var response = {
res.status(503).send("I'm busy right now, try again later."); res.status(503).send("I'm busy right now, try again later.");
}, },
newNote: newNote, newNote: newNote,
showFeatures: showFeatures,
showNote: showNote, showNote: showNote,
showPublishNote: showPublishNote, showPublishNote: showPublishNote,
showPublishSlide: showPublishSlide, showPublishSlide: showPublishSlide,
@ -67,8 +59,13 @@ function responseError(res, code, detail, msg) {
'Content-Type': 'text/html' 'Content-Type': 'text/html'
}); });
var template = config.errorpath; var template = config.errorpath;
var content = ejs.render(fs.readFileSync(template, 'utf8'), { var options = {
url: config.getserverurl(), cache: !config.debug,
filename: template
};
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
var content = compiled({
url: config.serverurl,
title: code + ' ' + detail + ' ' + msg, title: code + ' ' + detail + ' ' + msg,
cache: !config.debug, cache: !config.debug,
filename: template, filename: template,
@ -86,193 +83,163 @@ function showIndex(req, res, next) {
'Content-Type': 'text/html' 'Content-Type': 'text/html'
}); });
var template = config.indexpath; var template = config.indexpath;
var content = ejs.render(fs.readFileSync(template, 'utf8'), { var options = {
url: config.getserverurl(), cache: !config.debug,
useCDN: config.usecdn filename: template
};
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
var content = compiled({
url: config.serverurl,
useCDN: config.usecdn,
facebook: config.facebook,
twitter: config.twitter,
github: config.github,
dropbox: config.dropbox,
}); });
res.write(content); res.write(content);
res.end(); res.end();
} }
function responseHackMD(res, noteId) { function responseHackMD(res, note) {
db.readFromDB(noteId, function (err, data) { var body = LZString.decompressFromBase64(note.content);
if (err) { var meta = null;
return response.errorNotFound(res); try {
} meta = metaMarked(body).meta;
var notedata = data.rows[0]; } catch(err) {
var body = LZString.decompressFromBase64(notedata.content); //na
var meta = null; }
try { var title = models.Note.decodeTitle(note.title);
meta = metaMarked(body).meta; title = models.Note.generateWebTitle(title);
} catch(err) { var template = config.hackmdpath;
//na var options = {
} cache: !config.debug,
var title = Note.decodeTitle(notedata.title); filename: template
title = Note.generateWebTitle(title); };
var template = config.hackmdpath; var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
var options = { var html = compiled({
cache: !config.debug, url: config.serverurl,
filename: template title: title,
}; useCDN: config.usecdn,
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options); robots: (meta && meta.robots) || false, //default allow robots
var html = compiled({ facebook: config.facebook,
url: config.getserverurl(), twitter: config.twitter,
title: title, github: config.github,
useCDN: config.usecdn, dropbox: config.dropbox,
robots: (meta && meta.robots) || false //default allow robots
});
var buf = html;
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8',
'Cache-Control': 'private',
'Content-Length': buf.length
});
res.end(buf);
}); });
var buf = html;
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8',
'Cache-Control': 'private',
'Content-Length': buf.length
});
res.end(buf);
} }
function newNote(req, res, next) { function newNote(req, res, next) {
var newId = uuid.v4();
var body = fs.readFileSync(config.defaultnotepath, 'utf8');
body = LZString.compressToBase64(body);
var owner = null; var owner = null;
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
owner = req.user._id; owner = req.user.id;
} }
db.newToDB(newId, owner, body, function (err, result) { models.Note.create({
if (err) { ownerId: owner
return response.errorInternalError(res); }).then(function (note) {
} return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id));
Note.newNote(newId, owner, function(err, result) { }).catch(function (err) {
if (err) { logger.error(err);
return response.errorInternalError(res); return response.errorInternalError(res);
}
res.redirect(config.getserverurl() + "/" + LZString.compressToBase64(newId));
});
}); });
} }
function showFeatures(req, res, next) { function checkViewPermission(req, note) {
db.readFromDB(config.featuresnotename, function (err, data) { if (note.permission == 'private') {
if (err) { if (!req.isAuthenticated() || note.ownerId != req.user.id)
var body = fs.readFileSync(config.defaultfeaturespath, 'utf8'); return false;
body = LZString.compressToBase64(body); else
db.newToDB(config.featuresnotename, null, body, function (err, result) { return true;
if (err) { } else {
return response.errorInternalError(res); return true;
} }
responseHackMD(res, config.featuresnotename); }
});
} else { function findNote(req, res, callback, include) {
responseHackMD(res, config.featuresnotename); var id = req.params.noteId || req.params.shortid;
} models.Note.parseNoteId(id, function (err, _id) {
models.Note.findOne({
where: {
id: _id
},
include: include || null
}).then(function (note) {
if (!note) {
return response.errorNotFound(res);
}
if (!checkViewPermission(req, note)) {
return response.errorForbidden(res);
} else {
return callback(note);
}
}).catch(function (err) {
logger.error(err);
return response.errorInternalError(res);
});
}); });
} }
function showNote(req, res, next) { function showNote(req, res, next) {
var noteId = req.params.noteId; findNote(req, res, function (note) {
if (noteId != config.featuresnotename) { return responseHackMD(res, note);
if (!Note.checkNoteIdValid(noteId)) {
return response.errorNotFound(res);
}
noteId = LZString.decompressFromBase64(noteId);
if (!noteId) {
return response.errorNotFound(res);
}
}
db.readFromDB(noteId, function (err, data) {
if (err) {
return response.errorNotFound(res);
}
var notedata = data.rows[0];
Note.findOrNewNote(noteId, notedata.owner, function (err, note) {
if (err || !note) {
return response.errorNotFound(res);
}
//check view permission
if (note.permission == 'private') {
if (!req.isAuthenticated() || notedata.owner != req.user._id)
return response.errorForbidden(res);
}
responseHackMD(res, noteId);
});
}); });
} }
function showPublishNote(req, res, next) { function showPublishNote(req, res, next) {
var shortid = req.params.shortid; var include = [{
if (shortId.isValid(shortid)) { model: models.User,
Note.findNote(shortid, function (err, note) { as: "owner"
if (err || !note) { }, {
model: models.User,
as: "lastchangeuser"
}];
findNote(req, res, function (note) {
note.increment('viewcount').then(function (note) {
if (!note) {
return response.errorNotFound(res); return response.errorNotFound(res);
} }
db.readFromDB(note.id, function (err, data) { var body = LZString.decompressFromBase64(note.content);
if (err) { var meta = null;
return response.errorNotFound(res); try {
} meta = metaMarked(body).meta;
var notedata = data.rows[0]; } catch(err) {
//check view permission //na
if (note.permission == 'private') { }
if (!req.isAuthenticated() || notedata.owner != req.user._id) var createtime = note.createdAt;
return response.errorForbidden(res); var updatetime = note.lastchangeAt;
} var text = S(body).escapeHTML().s;
//increase note viewcount var title = models.Note.decodeTitle(note.title);
Note.increaseViewCount(note, function (err, note) { title = models.Note.generateWebTitle(title);
if (err || !note) { var origin = config.serverurl;
return response.errorNotFound(res); var data = {
} title: title,
var body = LZString.decompressFromBase64(notedata.content); viewcount: note.viewcount,
var meta = null; createtime: createtime,
try { updatetime: updatetime,
meta = metaMarked(body).meta; url: origin,
} catch(err) { body: text,
//na useCDN: config.usecdn,
} lastchangeuserprofile: note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null,
var updatetime = notedata.update_time; robots: (meta && meta.robots) || false //default allow robots
var text = S(body).escapeHTML().s; };
var title = Note.decodeTitle(notedata.title); return renderPublish(data, res);
title = Note.generateWebTitle(title); }).catch(function (err) {
var origin = config.getserverurl(); logger.error(err);
var data = { return response.errorInternalError(res);
title: title,
viewcount: note.viewcount,
updatetime: updatetime,
url: origin,
body: text,
useCDN: config.usecdn,
lastchangeuserprofile: null,
robots: (meta && meta.robots) || false //default allow robots
};
if (note.lastchangeuser) {
//find last change user profile if lastchangeuser exists
User.findUser(note.lastchangeuser, function (err, user) {
if (!err && user && user.profile) {
var profile = JSON.parse(user.profile);
if (profile) {
data.lastchangeuserprofile = {
name: profile.displayName || profile.username,
photo: User.parsePhotoByProfile(profile)
}
renderPublish(data, res);
}
}
});
} else {
renderPublish(data, res);
}
});
});
}); });
} else { }, include);
return response.errorNotFound(res);
}
} }
function renderPublish(data, res) { function renderPublish(data, res) {
var template = config.prettypath; var template = config.prettypath;
var options = { var options = {
url: config.getserverurl(), url: config.serverurl,
cache: !config.debug, cache: !config.debug,
filename: template filename: template
}; };
@ -287,343 +254,206 @@ function renderPublish(data, res) {
res.end(buf); res.end(buf);
} }
function actionPublish(req, res, noteId) { function actionPublish(req, res, note) {
db.readFromDB(noteId, function (err, data) { res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid));
if (err) {
return response.errorNotFound(res);
}
var owner = data.rows[0].owner;
Note.findOrNewNote(noteId, owner, function (err, note) {
if (err) {
return response.errorNotFound(res);
}
res.redirect(config.getserverurl() + "/s/" + note.shortid);
});
});
} }
function actionSlide(req, res, noteId) { function actionSlide(req, res, note) {
db.readFromDB(noteId, function (err, data) { res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid));
if (err) {
return response.errorNotFound(res);
}
var owner = data.rows[0].owner;
Note.findOrNewNote(noteId, owner, function (err, note) {
if (err) {
return response.errorNotFound(res);
}
res.redirect(config.getserverurl() + "/p/" + note.shortid);
});
});
} }
function actionDownload(req, res, noteId) { function actionDownload(req, res, note) {
db.readFromDB(noteId, function (err, data) { var body = LZString.decompressFromBase64(note.content);
if (err) { var title = models.Note.decodeTitle(note.title);
return response.errorNotFound(res); var filename = title;
} filename = encodeURIComponent(filename);
var notedata = data.rows[0]; res.writeHead(200, {
var body = LZString.decompressFromBase64(notedata.content); 'Access-Control-Allow-Origin': '*', //allow CORS as API
var title = Note.decodeTitle(notedata.title); 'Access-Control-Allow-Headers': 'Range',
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
'Content-Type': 'text/markdown; charset=UTF-8',
'Cache-Control': 'private',
'Content-disposition': 'attachment; filename=' + filename + '.md',
'Content-Length': body.length,
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
});
res.end(body);
}
function actionPDF(req, res, note) {
var body = LZString.decompressFromBase64(note.content);
try {
body = metaMarked(body).markdown;
} catch(err) {
//na
}
var title = models.Note.decodeTitle(note.title);
if (!fs.existsSync(config.tmppath)) {
fs.mkdirSync(config.tmppath);
}
var path = config.tmppath + Date.now() + '.pdf';
markdownpdf().from.string(body).to(path, function () {
var stream = fs.createReadStream(path);
var filename = title; var filename = title;
// Be careful of special characters
filename = encodeURIComponent(filename); filename = encodeURIComponent(filename);
res.writeHead(200, { // Ideally this should strip them
'Access-Control-Allow-Origin': '*', //allow CORS as API res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
'Access-Control-Allow-Headers': 'Range', res.setHeader('Cache-Control', 'private');
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
'Content-Type': 'text/markdown; charset=UTF-8', res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling
'Cache-Control': 'private', stream.pipe(res);
'Content-disposition': 'attachment; filename=' + filename + '.md', fs.unlink(path);
'Content-Length': body.length,
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
});
res.end(body);
}); });
} }
function actionPDF(req, res, noteId) { function actionGist(req, res, note) {
db.readFromDB(noteId, function (err, data) { var data = {
if (err) { client_id: config.github.clientID,
return response.errorNotFound(res); redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist',
} scope: "gist",
var notedata = data.rows[0]; state: shortId.generate()
var body = LZString.decompressFromBase64(notedata.content); };
try { var query = querystring.stringify(data);
body = metaMarked(body).markdown; res.redirect("https://github.com/login/oauth/authorize?" + query);
} catch(err) {
//na
}
var title = Note.decodeTitle(notedata.title);
if (!fs.existsSync(config.tmppath)) {
fs.mkdirSync(config.tmppath);
}
var path = config.tmppath + Date.now() + '.pdf';
markdownpdf().from.string(body).to(path, function () {
var stream = fs.createReadStream(path);
var filename = title;
// Be careful of special characters
filename = encodeURIComponent(filename);
// Ideally this should strip them
res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"');
res.setHeader('Cache-Control', 'private');
res.setHeader('Content-Type', 'application/pdf; charset=UTF-8');
res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling
stream.pipe(res);
fs.unlink(path);
});
});
}
function actionGist(req, res, noteId) {
db.readFromDB(noteId, function (err, data) {
if (err) {
return response.errorNotFound(res);
}
var owner = data.rows[0].owner;
Note.findOrNewNote(noteId, owner, function (err, note) {
if (err) {
return response.errorNotFound(res);
}
var data = {
client_id: config.github.clientID,
redirect_uri: config.getserverurl() + '/auth/github/callback/' + LZString.compressToBase64(noteId) + '/gist',
scope: "gist",
state: shortId.generate()
};
var query = querystring.stringify(data);
res.redirect("https://github.com/login/oauth/authorize?" + query);
});
});
} }
function noteActions(req, res, next) { function noteActions(req, res, next) {
var noteId = req.params.noteId; var noteId = req.params.noteId;
if (noteId != config.featuresnotename) { findNote(req, res, function (note) {
if (!Note.checkNoteIdValid(noteId)) { var action = req.params.action;
return response.errorNotFound(res); switch (action) {
case "publish":
case "pretty": //pretty deprecated
actionPublish(req, res, note);
break;
case "slide":
actionSlide(req, res, note);
break;
case "download":
actionDownload(req, res, note);
break;
case "pdf":
actionPDF(req, res, note);
break;
case "gist":
actionGist(req, res, note);
break;
default:
return res.redirect(config.serverurl + '/' + noteId);
break;
} }
noteId = LZString.decompressFromBase64(noteId);
if (!noteId) {
return response.errorNotFound(res);
}
}
Note.findNote(noteId, function (err, note) {
if (err || !note) {
return response.errorNotFound(res);
}
db.readFromDB(note.id, function (err, data) {
if (err) {
return response.errorNotFound(res);
}
var notedata = data.rows[0];
//check view permission
if (note.permission == 'private') {
if (!req.isAuthenticated() || notedata.owner != req.user._id)
return response.errorForbidden(res);
}
var action = req.params.action;
switch (action) {
case "publish":
case "pretty": //pretty deprecated
actionPublish(req, res, noteId);
break;
case "slide":
actionSlide(req, res, noteId);
break;
case "download":
actionDownload(req, res, noteId);
break;
case "pdf":
actionPDF(req, res, noteId);
break;
case "gist":
actionGist(req, res, noteId);
break;
default:
if (noteId != config.featuresnotename)
res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(noteId));
else
res.redirect(config.getserverurl() + '/' + noteId);
break;
}
});
}); });
} }
function publishNoteActions(req, res, next) { function publishNoteActions(req, res, next) {
var shortid = req.params.shortid; findNote(req, res, function (note) {
if (shortId.isValid(shortid)) { var action = req.params.action;
Note.findNote(shortid, function (err, note) { switch (action) {
if (err || !note) { case "edit":
return response.errorNotFound(res); res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id)));
} break;
db.readFromDB(note.id, function (err, data) { default:
if (err) { res.redirect(config.serverurl + '/s/' + note.shortid);
return response.errorNotFound(res); break;
} }
var notedata = data.rows[0]; });
//check view permission
if (note.permission == 'private') {
if (!req.isAuthenticated() || notedata.owner != req.user._id)
return response.errorForbidden(res);
}
var action = req.params.action;
switch (action) {
case "edit":
if (note.id != config.featuresnotename)
res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(note.id));
else
res.redirect(config.getserverurl() + '/' + note.id);
break;
}
});
});
}
} }
function githubActions(req, res, next) { function githubActions(req, res, next) {
var noteId = req.params.noteId; var noteId = req.params.noteId;
if (noteId != config.featuresnotename) { findNote(req, res, function (note) {
if (!Note.checkNoteIdValid(noteId)) { var action = req.params.action;
return response.errorNotFound(res); switch (action) {
case "gist":
githubActionGist(req, res, note);
break;
default:
res.redirect(config.serverurl + '/' + noteId);
break;
} }
noteId = LZString.decompressFromBase64(noteId);
if (!noteId) {
return response.errorNotFound(res);
}
}
Note.findNote(noteId, function (err, note) {
if (err || !note) {
return response.errorNotFound(res);
}
db.readFromDB(note.id, function (err, data) {
if (err) {
return response.errorNotFound(res);
}
var notedata = data.rows[0];
//check view permission
if (note.permission == 'private') {
if (!req.isAuthenticated() || notedata.owner != req.user._id)
return response.errorForbidden(res);
}
var action = req.params.action;
switch (action) {
case "gist":
githubActionGist(req, res, noteId);
break;
default:
if (noteId != config.featuresnotename)
res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(noteId));
else
res.redirect(config.getserverurl() + '/' + noteId);
break;
}
});
}); });
} }
function githubActionGist(req, res, noteId) { function githubActionGist(req, res, note) {
db.readFromDB(noteId, function (err, data) { var code = req.query.code;
if (err) { var state = req.query.state;
return response.errorNotFound(res); if (!code || !state) {
return response.errorForbidden(res);
} else {
var data = {
client_id: config.github.clientID,
client_secret: config.github.clientSecret,
code: code,
state: state
} }
var notedata = data.rows[0]; var auth_url = 'https://github.com/login/oauth/access_token';
var code = req.query.code; request({
var state = req.query.state; url: auth_url,
if (!code || !state) { method: "POST",
return response.errorForbidden(res); json: data
} else { }, function (error, httpResponse, body) {
var data = { if (!error && httpResponse.statusCode == 200) {
client_id: config.github.clientID, var access_token = body.access_token;
client_secret: config.github.clientSecret, if (access_token) {
code: code, var content = LZString.decompressFromBase64(note.content);
state: state var title = models.Note.decodeTitle(note.title);
} var filename = title.replace('/', ' ') + '.md';
var auth_url = 'https://github.com/login/oauth/access_token'; var gist = {
request({ "files": {}
url: auth_url, };
method: "POST", gist.files[filename] = {
json: data "content": content
}, function (error, httpResponse, body) { };
if (!error && httpResponse.statusCode == 200) { var gist_url = "https://api.github.com/gists";
var access_token = body.access_token; request({
if (access_token) { url: gist_url,
var content = LZString.decompressFromBase64(notedata.content); headers: {
var title = Note.decodeTitle(notedata.title); 'User-Agent': 'HackMD',
var filename = title.replace('/', ' ') + '.md'; 'Authorization': 'token ' + access_token
var gist = { },
"files": {} method: "POST",
}; json: gist
gist.files[filename] = { }, function (error, httpResponse, body) {
"content": content if (!error && httpResponse.statusCode == 201) {
}; res.setHeader('referer', '');
var gist_url = "https://api.github.com/gists"; res.redirect(body.html_url);
request({ } else {
url: gist_url, return response.errorForbidden(res);
headers: { }
'User-Agent': 'HackMD', });
'Authorization': 'token ' + access_token
},
method: "POST",
json: gist
}, function (error, httpResponse, body) {
if (!error && httpResponse.statusCode == 201) {
res.setHeader('referer', '');
res.redirect(body.html_url);
} else {
return response.errorForbidden(res);
}
});
} else {
return response.errorForbidden(res);
}
} else { } else {
return response.errorForbidden(res); return response.errorForbidden(res);
} }
}) } else {
} return response.errorForbidden(res);
}); }
})
}
} }
function showPublishSlide(req, res, next) { function showPublishSlide(req, res, next) {
var shortid = req.params.shortid; findNote(req, res, function (note) {
if (shortId.isValid(shortid)) { note.increment('viewcount').then(function (note) {
Note.findNote(shortid, function (err, note) { if (!note) {
if (err || !note) {
return response.errorNotFound(res); return response.errorNotFound(res);
} }
db.readFromDB(note.id, function (err, data) { var body = LZString.decompressFromBase64(note.content);
if (err) { try {
return response.errorNotFound(res); body = metaMarked(body).markdown;
} } catch(err) {
var notedata = data.rows[0]; //na
//check view permission }
if (note.permission == 'private') { var title = models.Note.decodeTitle(note.title);
if (!req.isAuthenticated() || notedata.owner != req.user._id) title = models.Note.generateWebTitle(title);
return response.errorForbidden(res); var text = S(body).escapeHTML().s;
} render(res, title, text);
//increase note viewcount }).catch(function (err) {
Note.increaseViewCount(note, function (err, note) { logger.error(err);
if (err || !note) { return response.errorInternalError(res);
return response.errorNotFound(res);
}
var body = LZString.decompressFromBase64(notedata.content);
try {
body = metaMarked(body).markdown;
} catch(err) {
//na
}
var title = Note.decodeTitle(notedata.title);
title = Note.generateWebTitle(title);
var text = S(body).escapeHTML().s;
render(res, title, text);
});
});
}); });
} else { });
return response.errorNotFound(res);
}
} }
//reveal.js render //reveal.js render
@ -631,7 +461,7 @@ var render = function (res, title, markdown) {
var slides = md.slidify(markdown, opts); var slides = md.slidify(markdown, opts);
res.end(Mustache.to_html(opts.template, { res.end(Mustache.to_html(opts.template, {
url: config.getserverurl(), url: config.serverurl,
title: title, title: title,
theme: opts.theme, theme: opts.theme,
highlightTheme: opts.highlightTheme, highlightTheme: opts.highlightTheme,

View file

@ -1,84 +0,0 @@
//temp
//external modules
var mongoose = require('mongoose');
//core
var config = require("../config.js");
var logger = require("./logger.js");
// create a temp model
var model = mongoose.model('temp', {
id: String,
data: String,
created: Date
});
//public
var temp = {
model: model,
findTemp: findTemp,
newTemp: newTemp,
removeTemp: removeTemp,
getTempCount: getTempCount
};
function getTempCount(callback) {
model.count(function(err, count){
if(err) callback(err, null);
else callback(null, count);
});
}
function findTemp(id, callback) {
model.findOne({
id: id
}, function (err, temp) {
if (err) {
logger.error('find temp failed: ' + err);
callback(err, null);
}
if (!err && temp) {
callback(null, temp);
} else {
logger.error('find temp failed: ' + err);
callback(err, null);
};
});
}
function newTemp(id, data, callback) {
var temp = new model({
id: id,
data: data,
created: Date.now()
});
temp.save(function (err) {
if (err) {
logger.error('new temp failed: ' + err);
callback(err, null);
} else {
logger.info("new temp success: " + temp.id);
callback(null, temp);
};
});
}
function removeTemp(id, callback) {
findTemp(id, function(err, temp) {
if(!err && temp) {
temp.remove(function(err) {
if(err) {
logger.error('remove temp failed: ' + err);
callback(err, null);
} else {
callback(null, null);
}
});
} else {
logger.error('remove temp failed: ' + err);
callback(err, null);
}
});
}
module.exports = temp;

View file

@ -1,110 +0,0 @@
//user
//external modules
var mongoose = require('mongoose');
var md5 = require("md5");
//core
var config = require("../config.js");
var logger = require("./logger.js");
// create a user model
var model = mongoose.model('user', {
id: String,
profile: String,
history: String,
created: Date
});
//public
var user = {
model: model,
findUser: findUser,
newUser: newUser,
findOrNewUser: findOrNewUser,
getUserCount: getUserCount,
parsePhotoByProfile: parsePhotoByProfile
};
function parsePhotoByProfile(profile) {
var photo = null;
switch (profile.provider) {
case "facebook":
photo = 'https://graph.facebook.com/' + profile.id + '/picture';
break;
case "twitter":
photo = profile.photos[0].value;
break;
case "github":
photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48';
break;
case "dropbox":
//no image api provided, use gravatar
photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
break;
}
return photo;
}
function getUserCount(callback) {
model.count(function(err, count){
if(err) callback(err, null);
else callback(null, count);
});
}
function findUser(id, callback) {
var rule = {};
var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
if (checkForHexRegExp.test(id))
rule._id = id;
else
rule.id = id;
model.findOne(rule, function (err, user) {
if (err) {
logger.error('find user failed: ' + err);
callback(err, null);
}
if (!err && user) {
callback(null, user);
} else {
logger.error('find user failed: ' + err);
callback(err, null);
};
});
}
function newUser(id, profile, callback) {
var user = new model({
id: id,
profile: JSON.stringify(profile),
created: Date.now()
});
user.save(function (err) {
if (err) {
logger.error('new user failed: ' + err);
callback(err, null);
} else {
logger.info("new user success: " + user.id);
callback(null, user);
};
});
}
function findOrNewUser(id, profile, callback) {
findUser(id, function(err, user) {
if(err || !user) {
newUser(id, profile, function(err, user) {
if(err) {
logger.error('find or new user failed: ' + err);
callback(err, null);
} else {
callback(null, user);
}
});
} else {
callback(null, user);
}
});
}
module.exports = user;

View file

@ -13,7 +13,7 @@
"chance": "^1.0.1", "chance": "^1.0.1",
"cheerio": "^0.20.0", "cheerio": "^0.20.0",
"compression": "^1.6.1", "compression": "^1.6.1",
"connect-mongo": "^1.1.0", "connect-session-sequelize": "^3.0.0",
"cookie": "0.2.3", "cookie": "0.2.3",
"cookie-parser": "1.4.1", "cookie-parser": "1.4.1",
"ejs": "^2.4.1", "ejs": "^2.4.1",
@ -25,16 +25,15 @@
"highlight.js": "^9.2.0", "highlight.js": "^9.2.0",
"imgur": "^0.1.7", "imgur": "^0.1.7",
"jsdom-nogyp": "^0.8.3", "jsdom-nogyp": "^0.8.3",
"kerberos": "0.0.19",
"lz-string": "1.4.4", "lz-string": "1.4.4",
"markdown-pdf": "^7.0.0", "markdown-pdf": "^7.0.0",
"marked": "^0.3.5", "marked": "^0.3.5",
"meta-marked": "^0.4.0", "meta-marked": "^0.4.0",
"method-override": "^2.3.5", "method-override": "^2.3.5",
"moment": "^2.12.0", "moment": "^2.12.0",
"mongoose": "^4.4.7",
"morgan": "^1.7.0", "morgan": "^1.7.0",
"mustache": "2.2.1", "mustache": "2.2.1",
"mysql": "^2.10.2",
"node-uuid": "^1.4.7", "node-uuid": "^1.4.7",
"passport": "^0.3.2", "passport": "^0.3.2",
"passport-dropbox-oauth2": "^1.0.0", "passport-dropbox-oauth2": "^1.0.0",
@ -42,13 +41,17 @@
"passport-github": "^1.1.0", "passport-github": "^1.1.0",
"passport-twitter": "^1.0.4", "passport-twitter": "^1.0.4",
"passport.socketio": "^3.6.1", "passport.socketio": "^3.6.1",
"pg": "4.x",
"randomcolor": "^0.4.3", "randomcolor": "^0.4.3",
"request": "^2.69.0", "request": "^2.69.0",
"pg": "^4.5.3",
"pg-hstore": "^2.3.2",
"reveal.js": "3.2.0", "reveal.js": "3.2.0",
"shortid": "2.2.4", "shortid": "2.2.4",
"sequelize": "^3.21.0",
"socket.io": "1.4.5", "socket.io": "1.4.5",
"sqlite3": "^3.1.3",
"string": "^3.3.1", "string": "^3.3.1",
"tedious": "^1.14.0",
"toobusy-js": "^0.4.3", "toobusy-js": "^0.4.3",
"winston": "^2.2.0" "winston": "^2.2.0"
}, },

View file

@ -3,7 +3,7 @@ Features
Introduction Introduction
=== ===
<i class="fa fa-file-text"></i> HackMD is a realtime collaborate markdown note in all platforms. <i class="fa fa-file-text"></i> **HackMD** is a realtime collaborate markdown note in all platforms.
This mean you can do some notes with any other in **Desktop, Tablet or even Phone**. This mean you can do some notes with any other in **Desktop, Tablet or even Phone**.
You can Sign in via **Facebook, Twitter, GitHub, Dropbox** in the **[homepage](/)**. You can Sign in via **Facebook, Twitter, GitHub, Dropbox** in the **[homepage](/)**.
@ -37,11 +37,11 @@ If you want to share a **editable** note, just copy the url.
If you want to share a **read-only** note, simply press share button <i class="fa fa-share-alt"></i> and copy the url. If you want to share a **read-only** note, simply press share button <i class="fa fa-share-alt"></i> and copy the url.
## Save ## Save
Currently, you can save to **dropbox** <i class="fa fa-dropbox"></i> or save as **.md** <i class="fa fa-file-text"></i> to local. Currently, you can save to **Dropbox** <i class="fa fa-dropbox"></i> or save as **.md** <i class="fa fa-file-text"></i> to local.
## Import ## Import
Like save feature, you can also import **.md** from **dropbox** <i class="fa fa-dropbox"></i>. Like save feature, you can also import **.md** from **Dropbox** <i class="fa fa-dropbox"></i>.
Or import from your **clipboard** <i class="fa fa-clipboard"></i>, and that can parse some **html** which might be useful :smiley: Or import from your **Clipboard** <i class="fa fa-clipboard"></i>, and that can parse some **html** which might be useful :smiley:
## Permission ## Permission
There is a little button on the top right of the view. There is a little button on the top right of the view.
@ -60,6 +60,11 @@ It might be one of below:
<iframe width="100%" height="500" src="http://hackmd.io/features" frameborder="0"></iframe> <iframe width="100%" height="500" src="http://hackmd.io/features" frameborder="0"></iframe>
``` ```
## [Slide Mode](./slide-example)
You can use some syntax to divide your note into slides.
Then use **Slide Mode** <i class="fa fa-tv"></i> to made a presentation.
Visit above link for detail.
View View
=== ===
## Table of content ## Table of content
@ -93,12 +98,13 @@ This will take the first **level 1 header** as the note title.
Using tags like below, these will show in your **history**. Using tags like below, these will show in your **history**.
###### tags: `features` `cool` `updated` ###### tags: `features` `cool` `updated`
## [YAML metadata](https://hackmd.io/IwFgZgxiBsBGCsBaAnPYAORJm07gDMImGAKYnrwDsUI8QA==) ## [YAML metadata](./yaml-metadata)
Provide advanced note information to set the browse behavior, visit above link for detail Provide advanced note information to set the browse behavior, visit above link for detail
- robots: set search engine to index or not - robots: set web robots meta
- lang: set browse language - lang: set browse language
- dir: set text direction - dir: set text direction
- breaks: set to use line breaks - breaks: set to use line breaks or not
- mathjax: set to parse mathjax or not
## Emoji ## Emoji
You can type any emoji like this :smile: :smiley: :cry: :wink: You can type any emoji like this :smile: :smiley: :cry: :wink:
@ -241,9 +247,41 @@ digraph hierarchy {
} }
``` ```
### Mermaid
```mermaid
gantt
title A Gantt Diagram
section Section
A task :a1, 2014-01-01, 30d
Another task :after a1 , 20d
section Another
Task in sec :2014-01-12 , 12d
anther task : 24d
```
> More information about **Sequence diagrams** syntax [here](http://bramp.github.io/js-sequence-diagrams/). > More information about **Sequence diagrams** syntax [here](http://bramp.github.io/js-sequence-diagrams/).
> More information about **Flow charts** syntax [here](http://adrai.github.io/flowchart.js/). > More information about **Flow charts** syntax [here](http://adrai.github.io/flowchart.js/).
> More information about **Graphviz** syntax [here](http://www.tonyballantyne.com/graphs.html) > More information about **Graphviz** syntax [here](http://www.tonyballantyne.com/graphs.html)
> More information about **Mermaid** syntax [here](http://knsv.github.io/mermaid)
Alert area
---
:::success
Yes :tada:
:::
:::info
This is a message :mega:
:::
:::warning
Watch out :zap:
:::
:::danger
Oh No :fire:
:::
## Typography ## Typography

View file

@ -0,0 +1,81 @@
Slide example
===
This feature still in beta, may have some issues.
For details:
https://github.com/hakimel/reveal.js/
---
## First slide
`---`
Is the divder of slides
----
### First branch of fisrt slide
`----`
Is the divder of branches
----
### Second branch of first slide
`<!-- .element: class="fragment" data-fragment-index="1" -->`
Is the fragment syntax
- Item 1<!-- .element: class="fragment" data-fragment-index="1" -->
- Item 2<!-- .element: class="fragment" data-fragment-index="2" -->
---
## Second slide
<!-- .slide: data-background="#1A237E" -->
`<!-- .slide: data-background="#1A237E" -->`
Is the background syntax
---
<!-- .slide: data-transition="zoom" -->
`<!-- .slide: data-transition="zoom" -->`
Is the transition syntax
you can use:
none/fade/slide/convex/concave/zoom
---
<!-- .slide: data-transition="fade-in convex-out" -->
`<!-- .slide: data-transition="fade-in convex-out" -->`
Also can set different in/out transition
you can use:
none/fade/slide/convex/concave/zoom
postfix with `-in` or `-out`
---
<!-- .slide: data-transition-speed="fast" -->
`<!-- .slide: data-transition-speed="fast" -->`
Custom the transition speed!
you can use:
default/fast/slow
---
# The End

View file

@ -0,0 +1,97 @@
---
robots: index, follow
lang: en
dir: ltr
breaks: true
---
Supported YAML metadata
===
First you need to insert syntax like this at the **start** of the note:
```
---
YAML metas
---
```
Replace the "YAML metas" in this section with any YAML options as below.
You can also refer to this note's source code.
robots
---
This option will give below meta in the note head meta:
```xml
<meta name="robots" content="your_meta">
```
So you can prevent any search engine index your note by set `noindex, nofollow`.
> default: not
**Example**
```xml
robots: noindex, nofollow
```
lang
---
This option will set the language of the note, that might alter some typography of it.
You can find your the language code in ISO 639-1 standard:
https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
> default: not set (which will be en)
**Example**
```xml
langs: ja-jp
```
dir
---
This option provide to describe the direction of the text in this note.
You can only use whether `rtl` or `ltr`.
Look more at here:
http://www.w3.org/International/questions/qa-html-dir
> default: not set (which will be ltr)
**Example**
```xml
dir: rtl
```
breaks
---
This option means the hardbreaks in the note will be parsed or be ignore.
The original markdown syntax breaks only if you put space twice, but HackMD choose to breaks every time you enter a break.
You can only use whether `true` or `false`.
> default: not set (which will be true)
**Example**
```xml
breaks: false
```
mathjax
---
This option let you to choose to parse mathjax syntax or not.
> default: not set (which will be true)
**Example**
```xml
mathjax: false
```
spellcheck
---
**Warning: Experimental feature!**
This option let you to choose to enable spell checking feature or not.
> default: not set (which will be false)
**Example**
```xml
spellcheck: true
```

View file

@ -1,237 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="mobile-web-app-capable" content="yes">
<meta name="description" content="Realtime collaborative markdown notes on all platforms.">
<meta name="author" content="jackycute">
<title>HackMD - Collaborative notes</title>
<link rel="icon" type="image/png" href="<%- url %>/favicon.png">
<link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
<!-- Bootstrap core CSS -->
<% if(useCDN) { %>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css">
<% } else { %>
<link rel="stylesheet" href="<%- url %>/vendor/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="<%- url %>/vendor/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="<%- url %>/css/bootstrap-social.css">
<% } %>
<link rel="stylesheet" href="<%- url %>/vendor/select2/select2.css">
<link rel="stylesheet" href="<%- url %>/vendor/select2/select2-bootstrap.css">
<!-- Custom styles for this template -->
<link rel="stylesheet" href="<%- url %>/css/cover.css">
<link rel="stylesheet" href="<%- url %>/css/site.css">
</head>
<body>
<div class="site-wrapper">
<div class="site-wrapper-inner">
<div class="cover-container">
<div class="masthead clearfix">
<div class="inner">
<h3 class="masthead-brand"></h3>
<nav>
<ul class="nav masthead-nav">
<li class="ui-home active"><a href="#">Home</a>
</li>
<li class="ui-history"><a href="#">History</a>
</li>
<li class="ui-releasenotes"><a href="#">Release Notes</a>
</li>
</ul>
</nav>
</div>
</div>
<div id="home" class="section">
<div class="inner cover">
<h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
<p class="lead">
Realtime collaborative markdown notes on all platforms.
</p>
<a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="display:none;">Sign In</a>
<div class="ui-or" style="display:none;">Or</div>
<p class="lead">
<a href="<%- url %>/new" class="btn btn-lg btn-default">New note</a>
</p>
<h5>Share directly with URL <i class="fa fa-link"></i></h5>
<a class="btn btn-primary" href="<%- url %>/features">More features <i class="fa fa-chevron-right"></i></a>
</div>
<br>
</div>
<div id="history" class="section" style="display:none;">
<div class="ui-signin">
<h4>
<a type="button" class="btn btn-success" data-toggle="modal" data-target=".signin-modal">Sign In</a> to get own history!
</h4>
<p>Below are history from browser</p>
</div>
<div class="ui-signout" style="display:none;">
<h4 class="ui-welcome">Welcome! <span class="ui-name"></span></h4>
<a href="<%- url %>/new" class="btn btn-default">New note</a> Or
<a href="#" class="btn btn-danger ui-logout">Sign Out</a>
</div>
<hr>
<form class="form-inline">
<div class="form-group" style="vertical-align: bottom;">
<input class="form-control ui-use-tags" style="min-width:172px;max-width:344px;" />
</div>
<div class="form-group">
<input class="search form-control" placeholder="Search anything..." />
</div>
<a href="#" class="sort btn btn-default" data-sort="text" title="Sort by title">
Title
</a>
<a href="#" class="sort btn btn-default" data-sort="timestamp" title="Sort by time">
Time
</a>
<span class="hidden-xs hidden-sm">
<a href="#" class="btn btn-default ui-save-history" title="Export history"><i class="fa fa-save"></i></a>
<span class="btn btn-default btn-file ui-open-history" title="Import history">
<i class="fa fa-folder-open-o"></i><input type="file" />
</span>
<a href="#" class="btn btn-default ui-clear-history" title="Clear history" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
</span>
<a href="#" class="btn btn-default ui-refresh-history" title="Refresh history"><i class="fa fa-refresh"></i></a>
</form>
<h4 class="ui-nohistory" style="display:none;">
No history
</h4>
<a href="#" class="btn btn-primary ui-import-from-browser" style="display:none;">Import from browser</a>
<ul id="history-list" class="list">
</ul>
</div>
<div id="releasenotes" class="section" style="display:none;">
<div id="template" style="display:none;">
{{#each release}}
<div class="inner cover">
<h5 class="cover-heading">
<div class="text-left">
<i class="fa fa-tag"></i> {{version}}
&nbsp;<span class="label label-default">{{tag}}</span>
<div class="pull-right">
<i class="fa fa-clock-o"></i> {{date}}
</div>
</div>
</h5>
<hr>{{#each detail}}
<div class="text-left">
<h5><i class="fa fa-dot-circle-o"></i> {{title}}</h5>
<ul>
{{#each item}}
<li>
{{this}}
</li>
{{/each}}
</ul>
</div>
{{/each}}
</div>
{{#unless @last}}
<br>{{/unless}} {{/each}}
</div>
</div>
<div class="mastfoot">
<div class="inner">
<h6>
<iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="90px" height="20px" style="vertical-align:middle;"></iframe>
</h6>
<p>&copy; 2016 <a href="https://www.facebook.com/TakeHackMD" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> by <a href="https://github.com/jackycute" target="_blank"><i class="fa fa-github-square"></i> jackycute</a>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- signin modal -->
<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="mySmallModalLabel">Choose method</h4>
</div>
<div class="modal-body">
<a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
<i class="fa fa-facebook"></i> Sign in via Facebook
</a>
<a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
<i class="fa fa-twitter"></i> Sign in via Twitter
</a>
<a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
<i class="fa fa-github"></i> Sign in via GitHub
</a>
<a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
<i class="fa fa-dropbox"></i> Sign in via Dropbox
</a>
</div>
</div>
</div>
</div>
<!-- delete modal -->
<div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="myModalLabel">Are you sure?</h4>
</div>
<div class="modal-body" style="color:black;">
<h5 class="ui-delete-modal-msg"></h5>
<strong class="ui-delete-modal-item"></strong>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger ui-delete-modal-confirm">Yes, do it!</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<% if(useCDN) { %>
<script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/jquery.gsap.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment-with-locales.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/js-url/2.0.2/url.min.js" defer></script>
<% } else { %>
<script src="<%- url %>/vendor/jquery/dist/jquery.min.js" defer></script>
<script src="<%- url %>/vendor/bootstrap/dist/js/bootstrap.min.js" defer></script>
<script src="<%- url %>/vendor/gsap/src/minified/TweenMax.min.js" defer></script>
<script src="<%- url %>/vendor/gsap/src/minified/jquery.gsap.min.js" defer></script>
<script src="<%- url %>/vendor/select2/select2.min.js" defer></script>
<script src="<%- url %>/vendor/moment/min/moment-with-locales.min.js" defer></script>
<script src="<%- url %>/vendor/handlebars/handlebars.min.js" defer></script>
<script src="<%- url %>/vendor/js-url/url.min.js" defer></script>
<% } %>
<script src="<%- url %>/vendor/js.cookie.js" defer></script>
<script src="<%- url %>/vendor/list.min.js" defer></script>
<script src="<%- url %>/vendor/FileSaver.min.js" defer></script>
<script src="<%- url %>/vendor/store.min.js" defer></script>
<script src="<%- url %>/vendor/lz-string/libs/lz-string.min.js" defer></script>
<script src="<%- url %>/js/common.js" defer></script>
<script src="<%- url %>/js/history.js" defer></script>
<script src="<%- url %>/js/cover.js" defer></script>
</body>
</html>

View file

@ -1,15 +1,24 @@
//auto update last change //auto update last change
var createtime = null;
var lastchangetime = null; var lastchangetime = null;
var lastchangeui = { var lastchangeui = {
status: $(".ui-status-lastchange"),
time: $(".ui-lastchange"), time: $(".ui-lastchange"),
user: $(".ui-lastchangeuser"), user: $(".ui-lastchangeuser"),
nouser: $(".ui-no-lastchangeuser") nouser: $(".ui-no-lastchangeuser")
} }
function updateLastChange() { function updateLastChange() {
if (lastchangetime && lastchangeui) { if (!lastchangeui) return;
lastchangeui.time.html(moment(lastchangetime).fromNow()); if (createtime) {
lastchangeui.time.attr('title', moment(lastchangetime).format('llll')); if (createtime && !lastchangetime) {
lastchangeui.status.text('created');
} else {
lastchangeui.status.text('changed');
}
var time = lastchangetime || createtime;
lastchangeui.time.html(moment(time).fromNow());
lastchangeui.time.attr('title', moment(time).format('llll'));
} }
} }
setInterval(updateLastChange, 60000); setInterval(updateLastChange, 60000);

View file

@ -93,8 +93,14 @@ function clearDuplicatedHistory(notehistory) {
for (var i = 0; i < notehistory.length; i++) { for (var i = 0; i < notehistory.length; i++) {
var found = false; var found = false;
for (var j = 0; j < newnotehistory.length; j++) { for (var j = 0; j < newnotehistory.length; j++) {
var id = LZString.decompressFromBase64(notehistory[i].id); var id = notehistory[i].id;
var newId = LZString.decompressFromBase64(newnotehistory[j].id); var newId = newnotehistory[j].id;
try {
id = LZString.decompressFromBase64(id);
newId = LZString.decompressFromBase64(newId);
} catch (err) {
// na
}
if (id == newId || notehistory[i].id == newnotehistory[j].id || !notehistory[i].id || !newnotehistory[j].id) { if (id == newId || notehistory[i].id == newnotehistory[j].id || !notehistory[i].id || !newnotehistory[j].id) {
var time = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'); var time = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a');
var newTime = moment(newnotehistory[j].time, 'MMMM Do YYYY, h:mm:ss a'); var newTime = moment(newnotehistory[j].time, 'MMMM Do YYYY, h:mm:ss a');

View file

@ -1423,7 +1423,7 @@ function updatePermission(newPermission) {
title = "Only owner can view & edit"; title = "Only owner can view & edit";
break; break;
} }
if (personalInfo.userid == owner) { if (personalInfo.userid && personalInfo.userid == owner) {
label += ' <i class="fa fa-caret-down"></i>'; label += ' <i class="fa fa-caret-down"></i>';
ui.infobar.permission.label.removeClass('disabled'); ui.infobar.permission.label.removeClass('disabled');
} else { } else {
@ -1476,11 +1476,14 @@ socket.emit = function () {
socket.on('info', function (data) { socket.on('info', function (data) {
console.error(data); console.error(data);
switch (data.code) { switch (data.code) {
case 403:
location.href = "./403";
break;
case 404: case 404:
location.href = "./404"; location.href = "./404";
break; break;
case 403: case 500:
location.href = "./403"; location.href = "./500";
break; break;
} }
}); });
@ -1517,11 +1520,15 @@ socket.on('version', function (data) {
}); });
function updateLastInfo(data) { function updateLastInfo(data) {
//console.log(data); //console.log(data);
if (lastchangetime !== data.updatetime) { if (data.hasOwnProperty('createtime') && createtime !== data.createtime) {
createtime = data.createtime;
updateLastChange();
}
if (data.hasOwnProperty('updatetime') && lastchangetime !== data.updatetime) {
lastchangetime = data.updatetime; lastchangetime = data.updatetime;
updateLastChange(); updateLastChange();
} }
if (lastchangeuser !== data.lastchangeuser) { if (data.hasOwnProperty('lastchangeuser') && lastchangeuser !== data.lastchangeuser) {
lastchangeuser = data.lastchangeuser; lastchangeuser = data.lastchangeuser;
lastchangeuserprofile = data.lastchangeuserprofile; lastchangeuserprofile = data.lastchangeuserprofile;
updateLastChangeUser(); updateLastChangeUser();

View file

@ -20,7 +20,8 @@ renderTOC(markdown);
generateToc('toc'); generateToc('toc');
generateToc('toc-affix'); generateToc('toc-affix');
smoothHashScroll(); smoothHashScroll();
lastchangetime = lastchangeui.time.text(); createtime = lastchangeui.time.attr('data-createtime');
lastchangetime = lastchangeui.time.attr('data-updatetime');
updateLastChange(); updateLastChange();
var url = window.location.pathname; var url = window.location.pathname;
$('.ui-edit').attr('href', url + '/edit'); $('.ui-edit').attr('href', url + '/edit');

View file

@ -8,7 +8,7 @@
<span> <span>
<span class="ui-lastchangeuser" style="display: none;">&thinsp;<i class="ui-user-icon small" data-toggle="tooltip" data-placement="right"></i></span> <span class="ui-lastchangeuser" style="display: none;">&thinsp;<i class="ui-user-icon small" data-toggle="tooltip" data-placement="right"></i></span>
<span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span> <span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span>
&nbsp;<span class="text-uppercase">changed</span> &nbsp;<span class="text-uppercase ui-status-lastchange"></span>
<span class="ui-lastchange text-uppercase"></span> <span class="ui-lastchange text-uppercase"></span>
</span> </span>
<span class="ui-permission dropdown pull-right"> <span class="ui-permission dropdown pull-right">
@ -73,32 +73,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- signin modal -->
<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="mySmallModalLabel">Please sign in to edit</h4>
</div>
<div class="modal-body">
<a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
<i class="fa fa-facebook"></i> Sign in via Facebook
</a>
<a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
<i class="fa fa-twitter"></i> Sign in via Twitter
</a>
<a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
<i class="fa fa-github"></i> Sign in via GitHub
</a>
<a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
<i class="fa fa-dropbox"></i> Sign in via Dropbox
</a>
</div>
</div>
</div>
</div>
<!-- locked modal --> <!-- locked modal -->
<div class="modal fade locked-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal fade locked-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">

15
public/views/hackmd.ejs Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<%- include head %>
</head>
<body>
<%- include header %>
<%- include body %>
<%- include footer %>
<%- include foot %>
</body>
</html>

View file

@ -38,8 +38,10 @@
</li> </li>
<li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a> <li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a>
</li> </li>
<% if(typeof github !== 'undefined' && github) { %>
<li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a> <li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
</li> </li>
<% } %>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Import</li> <li class="dropdown-header">Import</li>
<li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a> <li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>
@ -119,8 +121,10 @@
</li> </li>
<li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a> <li role="presentation"><a role="menuitem" class="ui-save-google-drive" tabindex="-1" href="#" target="_self"><i class="fa fa-cloud-upload fa-fw"></i> Google Drive</a>
</li> </li>
<% if(typeof github !== 'undefined' && github) { %>
<li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a> <li role="presentation"><a role="menuitem" class="ui-save-gist" tabindex="-1" href="#" target="_blank"><i class="fa fa-github fa-fw"></i> Gist</a>
</li> </li>
<% } %>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Import</li> <li class="dropdown-header">Import</li>
<li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a> <li role="presentation"><a role="menuitem" class="ui-import-dropbox" tabindex="-1" href="#" target="_self"><i class="fa fa-dropbox fa-fw"></i> Dropbox</a>

View file

@ -2,14 +2,210 @@
<html lang="en"> <html lang="en">
<head> <head>
<%- include head %> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="mobile-web-app-capable" content="yes">
<meta name="description" content="Realtime collaborative markdown notes on all platforms.">
<meta name="author" content="jackycute">
<title>HackMD - Collaborative notes</title>
<link rel="icon" type="image/png" href="<%- url %>/favicon.png">
<link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
<!-- Bootstrap core CSS -->
<% if(useCDN) { %>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css">
<% } else { %>
<link rel="stylesheet" href="<%- url %>/vendor/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="<%- url %>/vendor/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="<%- url %>/css/bootstrap-social.css">
<% } %>
<link rel="stylesheet" href="<%- url %>/vendor/select2/select2.css">
<link rel="stylesheet" href="<%- url %>/vendor/select2/select2-bootstrap.css">
<!-- Custom styles for this template -->
<link rel="stylesheet" href="<%- url %>/css/cover.css">
<link rel="stylesheet" href="<%- url %>/css/site.css">
</head> </head>
<body> <body>
<%- include header %> <div class="site-wrapper">
<%- include body %> <div class="site-wrapper-inner">
<%- include footer %> <div class="cover-container">
<%- include foot %>
<div class="masthead clearfix">
<div class="inner">
<h3 class="masthead-brand"></h3>
<nav>
<ul class="nav masthead-nav">
<li class="ui-home active"><a href="#">Home</a>
</li>
<li class="ui-history"><a href="#">History</a>
</li>
<li class="ui-releasenotes"><a href="#">Release Notes</a>
</li>
</ul>
</nav>
</div>
</div>
<div id="home" class="section">
<div class="inner cover">
<h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
<p class="lead">
Realtime collaborative markdown notes on all platforms.
</p>
<a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="display:none;">Sign In</a>
<div class="ui-or" style="display:none;">Or</div>
<p class="lead">
<a href="<%- url %>/new" class="btn btn-lg btn-default">New note</a>
</p>
<h5>Share directly with URL <i class="fa fa-link"></i></h5>
<a class="btn btn-primary" href="<%- url %>/features">More features <i class="fa fa-chevron-right"></i></a>
</div>
<br>
</div>
<div id="history" class="section" style="display:none;">
<div class="ui-signin">
<h4>
<a type="button" class="btn btn-success" data-toggle="modal" data-target=".signin-modal">Sign In</a> to get own history!
</h4>
<p>Below are history from browser</p>
</div>
<div class="ui-signout" style="display:none;">
<h4 class="ui-welcome">Welcome! <span class="ui-name"></span></h4>
<a href="<%- url %>/new" class="btn btn-default">New note</a> Or
<a href="#" class="btn btn-danger ui-logout">Sign Out</a>
</div>
<hr>
<form class="form-inline">
<div class="form-group" style="vertical-align: bottom;">
<input class="form-control ui-use-tags" style="min-width:172px;max-width:344px;" />
</div>
<div class="form-group">
<input class="search form-control" placeholder="Search anything..." />
</div>
<a href="#" class="sort btn btn-default" data-sort="text" title="Sort by title">
Title
</a>
<a href="#" class="sort btn btn-default" data-sort="timestamp" title="Sort by time">
Time
</a>
<span class="hidden-xs hidden-sm">
<a href="#" class="btn btn-default ui-save-history" title="Export history"><i class="fa fa-save"></i></a>
<span class="btn btn-default btn-file ui-open-history" title="Import history">
<i class="fa fa-folder-open-o"></i><input type="file" />
</span>
<a href="#" class="btn btn-default ui-clear-history" title="Clear history" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
</span>
<a href="#" class="btn btn-default ui-refresh-history" title="Refresh history"><i class="fa fa-refresh"></i></a>
</form>
<h4 class="ui-nohistory" style="display:none;">
No history
</h4>
<a href="#" class="btn btn-primary ui-import-from-browser" style="display:none;">Import from browser</a>
<ul id="history-list" class="list">
</ul>
</div>
<div id="releasenotes" class="section" style="display:none;">
<div id="template" style="display:none;">
{{#each release}}
<div class="inner cover">
<h5 class="cover-heading">
<div class="text-left">
<i class="fa fa-tag"></i> {{version}}
&nbsp;<span class="label label-default">{{tag}}</span>
<div class="pull-right">
<i class="fa fa-clock-o"></i> {{date}}
</div>
</div>
</h5>
<hr>{{#each detail}}
<div class="text-left">
<h5><i class="fa fa-dot-circle-o"></i> {{title}}</h5>
<ul>
{{#each item}}
<li>
{{this}}
</li>
{{/each}}
</ul>
</div>
{{/each}}
</div>
{{#unless @last}}
<br>{{/unless}} {{/each}}
</div>
</div>
<div class="mastfoot">
<div class="inner">
<h6>
<iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="90px" height="20px" style="vertical-align:middle;"></iframe>
</h6>
<p>&copy; 2016 <a href="https://www.facebook.com/TakeHackMD" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> by <a href="https://github.com/jackycute" target="_blank"><i class="fa fa-github-square"></i> jackycute</a>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- delete modal -->
<div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="myModalLabel">Are you sure?</h4>
</div>
<div class="modal-body" style="color:black;">
<h5 class="ui-delete-modal-msg"></h5>
<strong class="ui-delete-modal-item"></strong>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger ui-delete-modal-confirm">Yes, do it!</button>
</div>
</div>
</div>
</div>
<%- include modal %>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<% if(useCDN) { %>
<script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/jquery.gsap.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment-with-locales.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/js-url/2.0.2/url.min.js" defer></script>
<% } else { %>
<script src="<%- url %>/vendor/jquery/dist/jquery.min.js" defer></script>
<script src="<%- url %>/vendor/bootstrap/dist/js/bootstrap.min.js" defer></script>
<script src="<%- url %>/vendor/gsap/src/minified/TweenMax.min.js" defer></script>
<script src="<%- url %>/vendor/gsap/src/minified/jquery.gsap.min.js" defer></script>
<script src="<%- url %>/vendor/select2/select2.min.js" defer></script>
<script src="<%- url %>/vendor/moment/min/moment-with-locales.min.js" defer></script>
<script src="<%- url %>/vendor/handlebars/handlebars.min.js" defer></script>
<script src="<%- url %>/vendor/js-url/url.min.js" defer></script>
<% } %>
<script src="<%- url %>/vendor/js.cookie.js" defer></script>
<script src="<%- url %>/vendor/list.min.js" defer></script>
<script src="<%- url %>/vendor/FileSaver.min.js" defer></script>
<script src="<%- url %>/vendor/store.min.js" defer></script>
<script src="<%- url %>/vendor/lz-string/libs/lz-string.min.js" defer></script>
<script src="<%- url %>/js/common.js" defer></script>
<script src="<%- url %>/js/history.js" defer></script>
<script src="<%- url %>/js/cover.js" defer></script>
</body> </body>
</html> </html>

34
public/views/modal.ejs Normal file
View file

@ -0,0 +1,34 @@
<!-- signin modal -->
<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="mySmallModalLabel">Choose method</h4>
</div>
<div class="modal-body">
<% if(facebook) { %>
<a href="<%- url %>/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
<i class="fa fa-facebook"></i> Sign in via Facebook
</a>
<% } %>
<% if(twitter) { %>
<a href="<%- url %>/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
<i class="fa fa-twitter"></i> Sign in via Twitter
</a>
<% } %>
<% if(github) { %>
<a href="<%- url %>/auth/github" class="btn btn-lg btn-block btn-social btn-github">
<i class="fa fa-github"></i> Sign in via GitHub
</a>
<% } %>
<% if(dropbox) { %>
<a href="<%- url %>/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
<i class="fa fa-dropbox"></i> Sign in via Dropbox
</a>
<% } %>
</div>
</div>
</div>
</div>

View file

@ -49,8 +49,8 @@
<% } else { %> <% } else { %>
<span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span> <span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span>
<% } %> <% } %>
&nbsp;<span class="text-uppercase">changed</span> &nbsp;<span class="text-uppercase ui-status-lastchange"></span>
<span class="ui-lastchange text-uppercase"><%- updatetime %></span> <span class="ui-lastchange text-uppercase" data-createtime="<%- createtime %>" data-updatetime="<%- updatetime %>"></span>
</span> </span>
<span class="pull-right"><%- viewcount %> views <a href="#" class="ui-edit" title="Edit this note"><i class="fa fa-fw fa-pencil"></i></a></span> <span class="pull-right"><%- viewcount %> views <a href="#" class="ui-edit" title="Edit this note"><i class="fa fa-fw fa-pencil"></i></a></span>
</small> </small>

View file

@ -20,7 +20,7 @@
<script> <script>
document.write( '<link rel="stylesheet" href="{{{url}}}/vendor/reveal.js/css/print/' + ( window.location.search.match( /print-pdf/gi ) ? 'pdf' : 'paper' ) + '.css" type="text/css" media="print">' ); document.write( '<link rel="stylesheet" href="{{{url}}}/vendor/reveal.js/css/print/' + ( window.location.search.match( /print-pdf/gi ) ? 'pdf' : 'paper' ) + '.css" type="text/css" media="print">' );
</script> </script>
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script> <script src="{{{url}}}/vendor/jquery/dist/jquery.min.js"></script>
</head> </head>
<body> <body>

View file

@ -1,22 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory Listing</title>
<link rel="stylesheet" href="{{{url}}}/vendor/reveal.js/{{{theme}}}" id="theme">
<style type="text/css">
body {
margin: 1em;
}
a {
color: white;
display: block;
}
</style>
<link rel="icon" href="http://i.imgur.com/IVlU2PU.png" sizes="512x512" />
</head>
<body>
{{{listing}}}
</body>
</html>