diff options
author | Cheng-Han, Wu | 2016-04-20 18:03:55 +0800 |
---|---|---|
committer | Cheng-Han, Wu | 2016-04-20 18:03:55 +0800 |
commit | 49b51e478fa75b8d5254662de3265edcf8906004 (patch) | |
tree | 3b09213baae129156339b5ad496924f591790e88 | |
parent | e613aeba75aec5ceb4f10ae62881a3635183857d (diff) |
Refactor server with Sequelize ORM, refactor server configs, now will show note status (created or updated) and support docs (note alias)
35 files changed, 1879 insertions, 2122 deletions
@@ -9,6 +9,17 @@ Still in early stage, feel free to fork or contribute to this. 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 --- - Chrome >= 45, Chrome for Android >= 47 @@ -20,33 +31,24 @@ Browsers Requirement Prerequisite --- -- Node.js 4.x or up (test up to 5.8.0) -- PostgreSQL 9.3.x or 9.4.x -- MongoDB 3.0.x +- Node.js 4.x or up (test up to 5.10.1) +- Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) - npm and bower Get started --- 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 -3. Install PostgreSQL and MongoDB (yes, currently we need both) -4. Import database schema, see more on below -5. Setup the configs, see more on below -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. +3. Setup the configs, see more on below +4. Setup environment variables, which will overwrite the configs +5. Run the server as you like (node, forever, pm2) Structure --- ``` hackmd/ ├── tmp/ --- temporary files +├── docs/ --- document files ├── lib/ --- server libraries └── public/ --- client files ├── css/ --- css styles @@ -57,63 +59,58 @@ hackmd/ 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 -./public/js/index.js --- for client settings +./config.json --- for server settings ./public/js/common.js --- for client settings ``` -Client-side index.js settings +Client settings `common.js` --- | variables | example values | description | | --------- | ------ | ----------- | | 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 | | urlpath | `hackmd` | sub url path, like: `www.example.com/<urlpath>` | -Environment variables +Environment variables (will overwrite other server configs) --- | variables | example values | description | | --------- | ------ | ----------- | -| NODE_ENV | `production` or `development` | show current environment status | -| DATABASE_URL | `postgresql://user:pass@host:port/hackmd` | PostgreSQL connection string | -| MONGOLAB_URI | `mongodb://user:pass@host:port/hackmd` | MongoDB connection string | -| 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 +| NODE_ENV | `production` or `development` | set current environment (will apply correspond settings in the `config.json`) | +| PORT | `80` | web app port | +| DEBUG | `true` or `false` | set debug mode, show more logs | + +Server settings `config.json` --- | 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 | -| usecdn | `true` or `false` | set to use CDN resources or not | -| version | `0.3.2` | currnet version, must match same var in client side `index.js` | +| domain | `localhost` | domain name | +| urlpath | `hackmd` | sub url path, like `www.example.com/<urlpath>` | +| port | `80` | web app port | | alloworigin | `['localhost']` | domain name whitelist | -| sslkeypath | `./cert/client.key` | ssl key path | -| sslcertpath | `./cert/hackmd_io.crt` | ssl cert path | -| sslcapath | `['./cert/COMODORSAAddTrustCA.crt']` | ssl ca chain | -| dhparampath | `./cert/dhparam.pem` | ssl dhparam path | -| tmppath | `./tmp/` | temp file path | -| postgresqlstring | `postgresql://user:pass@host:port/hackmd` | PostgreSQL connection string, fallback to this when not set in environment | -| mongodbstring | `mongodb://user:pass@host:port/hackmd` | MongoDB connection string, fallback to this when not set in environment | +| usessl | `true` or `false` | set to use ssl server (if true will auto turn on `protocolusessl`) | +| protocolusessl | `true` or `false` | set to use ssl protocol for resources path | +| urladdport | `true` or `false` | set to add port on callback url (port 80 or 443 won't applied) | +| usecdn | `true` or `false` | set to use CDN resources or not | +| db | `{ "dialect": "sqlite", "storage": "./db.hackmd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) | +| sslkeypath | `./cert/client.key` | ssl key path (only need when you set usessl) | +| 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 | | sessionsecret | `secret` | cookie session secret | | 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 | | heartbeattimeout | `10000` | socket.io heartbeat timeout | | documentmaxlength | `100000` | note max length | @@ -122,8 +119,8 @@ Third-party integration api key settings --- | service | file path | description | | ------- | --------- | ----------- | -| facebook, twitter, github, dropbox | `config.js` | for signin | -| imgur | `config.js` | for image upload | +| facebook, twitter, github, dropbox | `config.json` | for signin | +| imgur | `config.json` | for image upload | | dropbox | `public/views/foot.ejs` | for chooser and saver | | google drive | `public/js/common.js` | for export and import | @@ -7,12 +7,10 @@ var passport = require('passport'); var methodOverride = require('method-override'); var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); -var mongoose = require('mongoose'); var compression = require('compression') var session = require('express-session'); -var MongoStore = require('connect-mongo')(session); +var SequelizeStore = require('connect-session-sequelize')(session.Store); var fs = require('fs'); -var shortid = require('shortid'); var imgur = require('imgur'); var formidable = require('formidable'); var morgan = require('morgan'); @@ -20,12 +18,11 @@ var passportSocketIo = require("passport.socketio"); var helmet = require('helmet'); //core -var config = require("./config.js"); +var config = require("./lib/config.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 response = require("./lib/response.js"); +var models = require("./lib/models"); //server setup if (config.usessl) { @@ -60,11 +57,7 @@ app.use(morgan('combined', { //socket io var io = require('socket.io')(server); -// connect to the mongodb -mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring); - //others -var db = require("./lib/db.js"); var realtime = require("./lib/realtime.js"); //assign socket io to realtime @@ -82,13 +75,9 @@ var urlencodedParser = bodyParser.urlencoded({ }); //session store -var sessionStore = new MongoStore({ - mongooseConnection: mongoose.connection, - touchAfter: config.sessiontouch - }, - function (err) { - logger.info(err); - }); +var sessionStore = new SequelizeStore({ + db: models.sequelize +}); //compression app.use(compression()); @@ -139,15 +128,21 @@ app.use(passport.session()); //serialize and deserialize passport.serializeUser(function (user, done) { - //logger.info('serializeUser: ' + user._id); - done(null, user._id); + logger.info('serializeUser: ' + user.id); + return done(null, user.id); }); passport.deserializeUser(function (id, done) { - User.model.findById(id, function (err, user) { - //logger.info(user) - if (!err) done(null, user); - else done(err, null); - }) + models.User.findOne({ + where: { + id: id + } + }).then(function (user) { + logger.info('deserializeUser: ' + user.id); + return done(null, user); + }).catch(function (err) { + logger.error(err); + return done(err, null); + }); }); //routes @@ -161,13 +156,17 @@ app.engine('html', ejs.renderFile); //get index app.get("/", response.showIndex); //get 403 forbidden -app.get("/403", function(req, res) { +app.get("/403", function (req, res) { response.errorForbidden(res); }); //get 404 not found -app.get("/404", function(req, res) { +app.get("/404", function (req, res) { response.errorNotFound(res); }); +//get 500 internal error +app.get("/500", function (req, res) { + response.errorInternalError(res); +}); //get status app.get("/status", function (req, res, next) { realtime.getStatus(function (data) { @@ -184,19 +183,26 @@ app.get("/temp", function (req, res) { if (!tempid) response.errorForbidden(res); else { - Temp.findTemp(tempid, function (err, temp) { - if (err || !temp) - response.errorForbidden(res); + models.Temp.findOne({ + where: { + id: tempid + } + }).then(function (temp) { + if (!temp) + response.errorNotFound(res); else { res.header("Access-Control-Allow-Origin", "*"); res.send({ temp: temp.data }); - temp.remove(function (err) { + temp.destroy().catch(function (err) { if (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) response.errorForbidden(res); else { - var id = shortid.generate(); var data = req.body.data; - if (!id || !data) + if (!data) response.errorForbidden(res); else { if (config.debug) logger.info('SERVER received temp from [' + host + ']: ' + req.body.data); - Temp.newTemp(id, data, function (err, temp) { - if (!err && temp) { + models.Temp.create({ + data: data + }).then(function (temp) { + if (temp) { res.header("Access-Control-Allow-Origin", "*"); res.send({ status: 'ok', @@ -223,125 +230,149 @@ app.post("/temp", urlencodedParser, function (req, res) { }); } else response.errorInternalError(res); + }).catch(function (err) { + logger.error(err); + return response.errorInternalError(res); }); } } }); //facebook auth -app.get('/auth/facebook', - passport.authenticate('facebook'), - function (req, res) {}); -//facebook auth callback -app.get('/auth/facebook/callback', - passport.authenticate('facebook', { - failureRedirect: config.getserverurl() - }), - function (req, res) { - res.redirect(config.getserverurl()); - }); +if (config.facebook) { + app.get('/auth/facebook', + passport.authenticate('facebook'), + function (req, res) {}); + //facebook auth callback + app.get('/auth/facebook/callback', + passport.authenticate('facebook', { + failureRedirect: config.serverurl + }), + function (req, res) { + res.redirect(config.serverurl); + }); +} //twitter auth -app.get('/auth/twitter', - passport.authenticate('twitter'), - function (req, res) {}); -//twitter auth callback -app.get('/auth/twitter/callback', - passport.authenticate('twitter', { - failureRedirect: config.getserverurl() - }), - function (req, res) { - res.redirect(config.getserverurl()); - }); +if (config.twitter) { + app.get('/auth/twitter', + passport.authenticate('twitter'), + function (req, res) {}); + //twitter auth callback + app.get('/auth/twitter/callback', + passport.authenticate('twitter', { + failureRedirect: config.serverurl + }), + function (req, res) { + res.redirect(config.serverurl); + }); +} //github auth -app.get('/auth/github', - passport.authenticate('github'), - function (req, res) {}); -//github auth callback -app.get('/auth/github/callback', - passport.authenticate('github', { - failureRedirect: config.getserverurl() - }), - function (req, res) { - res.redirect(config.getserverurl()); - }); -//github callback actions -app.get('/auth/github/callback/:noteId/:action', response.githubActions); +if (config.github) { + app.get('/auth/github', + passport.authenticate('github'), + function (req, res) {}); + //github auth callback + app.get('/auth/github/callback', + passport.authenticate('github', { + failureRedirect: config.serverurl + }), + function (req, res) { + res.redirect(config.serverurl); + }); + //github callback actions + app.get('/auth/github/callback/:noteId/:action', response.githubActions); +} //dropbox auth -app.get('/auth/dropbox', - passport.authenticate('dropbox-oauth2'), - function (req, res) {}); -//dropbox auth callback -app.get('/auth/dropbox/callback', - passport.authenticate('dropbox-oauth2', { - failureRedirect: config.getserverurl() - }), - function (req, res) { - res.redirect(config.getserverurl()); - }); +if (config.dropbox) { + app.get('/auth/dropbox', + passport.authenticate('dropbox-oauth2'), + function (req, res) {}); + //dropbox auth callback + app.get('/auth/dropbox/callback', + passport.authenticate('dropbox-oauth2', { + failureRedirect: config.serverurl + }), + function (req, res) { + res.redirect(config.serverurl); + }); +} //logout app.get('/logout', function (req, res) { if (config.debug && req.isAuthenticated()) - logger.info('user logout: ' + req.user._id); + logger.info('user logout: ' + req.user.id); req.logout(); - res.redirect(config.getserverurl()); + res.redirect(config.serverurl); }); //get history app.get('/history', function (req, res) { if (req.isAuthenticated()) { - User.model.findById(req.user._id, function (err, user) { - if (err) { - logger.error('read history failed: ' + err); - } else { - var history = []; - if (user.history) - history = JSON.parse(user.history); - res.send({ - history: history - }); + models.User.findOne({ + where: { + id: req.user.id } + }).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 { - response.errorForbidden(res); + return response.errorForbidden(res); } }); //post history app.post('/history', urlencodedParser, function (req, res) { if (req.isAuthenticated()) { if (config.debug) - logger.info('SERVER received history from [' + req.user._id + ']: ' + req.body.history); - User.model.findById(req.user._id, function (err, user) { - if (err) { - logger.error('write history failed: ' + err); - } else { - user.history = req.body.history; - 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); - }; - }); + logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history); + models.User.update({ + history: req.body.history + }, { + where: { + id: req.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(); } else { - response.errorForbidden(res); + return response.errorForbidden(res); } }); //get me info app.get('/me', function (req, res) { if (req.isAuthenticated()) { - User.model.findById(req.user._id, function (err, user) { - if (err) { - logger.error('read me failed: ' + err); - } else { - var profile = JSON.parse(user.profile); - res.send({ - status: 'ok', - id: req.user._id, - name: profile.displayName || profile.username - }); + models.User.findOne({ + where: { + id: req.user.id } + }).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 { res.send({ @@ -370,19 +401,17 @@ app.post('/uploadimage', function (req, res) { }) .catch(function (err) { logger.error(err); - res.send('upload image error'); + return res.send('upload image error'); }); } catch (err) { logger.error(err); - res.send('upload image error'); + return res.send('upload image error'); } } }); }); //get new note app.get("/new", response.newNote); -//get features -app.get("/features", response.showFeatures); //get publish note app.get("/s/:shortid", response.showPublishNote); //publish note actions @@ -412,15 +441,22 @@ io.set('heartbeat timeout', config.heartbeattimeout); io.sockets.on('connection', realtime.connection); //listen -if (config.usessl) { - server.listen(config.sslport, function () { - logger.info('HTTPS Server listening at sslport %d', config.sslport); - }); -} else { - server.listen(config.port, function () { - logger.info('HTTP Server listening at port %d', config.port); - }); +function startListen() { + if (config.usessl) { + server.listen(config.port, function () { + logger.info('HTTPS Server listening at port %d', config.port); + }); + } else { + 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) { logger.error(err); -}); +});
\ No newline at end of file diff --git a/config.js b/config.js deleted file mode 100644 index 972fc3b4..00000000 --- a/config.js +++ /dev/null @@ -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; diff --git a/config.json b/config.json new file mode 100644 index 00000000..16ad258b --- /dev/null +++ b/config.json @@ -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" + } + } +}
\ No newline at end of file diff --git a/hackmd_schema.sql b/hackmd_schema.sql deleted file mode 100644 index ac7d671a..00000000 --- a/hackmd_schema.sql +++ /dev/null @@ -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 --- - diff --git a/lib/auth.js b/lib/auth.js index dc8b94ca..af3e8d1d 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -7,44 +7,60 @@ var GithubStrategy = require('passport-github').Strategy; var DropboxStrategy = require('passport-dropbox-oauth2').Strategy; //core -var User = require('./user.js'); -var config = require('../config.js'); +var config = require('./config.js'); var logger = require("./logger.js"); +var models = require("./models"); function callback(accessToken, refreshToken, profile, done) { //logger.info(profile.displayName || profile.username); - User.findOrNewUser(profile.id, profile, function (err, user) { - if (err || user == null) { - logger.error('auth callback failed: ' + err); - } else { - if (config.debug && user) - logger.info('user login: ' + user._id); - done(null, user); + models.User.findOrCreate({ + where: { + profileid: profile.id.toString() + }, + defaults: { + profile: JSON.stringify(profile) } - }); + }).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 -module.exports = passport.use(new FacebookStrategy({ - clientID: config.facebook.clientID, - clientSecret: config.facebook.clientSecret, - callbackURL: config.getserverurl() + config.facebook.callbackPath -}, callback)); +if (config.facebook) { + module.exports = passport.use(new FacebookStrategy({ + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.serverurl + '/auth/facebook/callback' + }, callback)); +} //twitter -passport.use(new TwitterStrategy({ - consumerKey: config.twitter.consumerKey, - consumerSecret: config.twitter.consumerSecret, - callbackURL: config.getserverurl() + config.twitter.callbackPath -}, callback)); +if (config.twitter) { + passport.use(new TwitterStrategy({ + consumerKey: config.twitter.consumerKey, + consumerSecret: config.twitter.consumerSecret, + callbackURL: config.serverurl + '/auth/twitter/callback' + }, callback)); +} //github -passport.use(new GithubStrategy({ - clientID: config.github.clientID, - clientSecret: config.github.clientSecret, - callbackURL: config.getserverurl() + config.github.callbackPath -}, callback)); +if (config.github) { + passport.use(new GithubStrategy({ + clientID: config.github.clientID, + clientSecret: config.github.clientSecret, + callbackURL: config.serverurl + '/auth/github/callback' + }, callback)); +} //dropbox -passport.use(new DropboxStrategy({ - clientID: config.dropbox.clientID, - clientSecret: config.dropbox.clientSecret, - callbackURL: config.getserverurl() + config.dropbox.callbackPath -}, callback));
\ No newline at end of file +if (config.dropbox) { + passport.use(new DropboxStrategy({ + clientID: config.dropbox.clientID, + clientSecret: config.dropbox.clientSecret, + callbackURL: config.serverurl + '/auth/dropbox/callback' + }, callback)); +}
\ No newline at end of file diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 00000000..386b0885 --- /dev/null +++ b/lib/config.js @@ -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 +};
\ No newline at end of file diff --git a/lib/db.js b/lib/db.js deleted file mode 100644 index 1ac6f0e1..00000000 --- a/lib/db.js +++ /dev/null @@ -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;
\ No newline at end of file diff --git a/lib/models/index.js b/lib/models/index.js new file mode 100644 index 00000000..3b49d459 --- /dev/null +++ b/lib/models/index.js @@ -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;
\ No newline at end of file diff --git a/lib/models/note.js b/lib/models/note.js new file mode 100644 index 00000000..96043b75 --- /dev/null +++ b/lib/models/note.js @@ -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; +};
\ No newline at end of file diff --git a/lib/models/temp.js b/lib/models/temp.js new file mode 100644 index 00000000..6eeff153 --- /dev/null +++ b/lib/models/temp.js @@ -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; +};
\ No newline at end of file diff --git a/lib/models/user.js b/lib/models/user.js new file mode 100644 index 00000000..e1a373d6 --- /dev/null +++ b/lib/models/user.js @@ -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; +};
\ No newline at end of file diff --git a/lib/note.js b/lib/note.js deleted file mode 100644 index 0965b23e..00000000 --- a/lib/note.js +++ /dev/null @@ -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;
\ No newline at end of file diff --git a/lib/ot/server.js b/lib/ot/server.js index b6559291..227eba25 100644 --- a/lib/ot/server.js +++ b/lib/ot/server.js @@ -1,4 +1,4 @@ -var config = require('../../config'); +var config = require('../config'); if (typeof ot === 'undefined') { var ot = {}; diff --git a/lib/realtime.js b/lib/realtime.js index 081d401e..a8bef97a 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -5,24 +5,19 @@ var cookieParser = require('cookie-parser'); var url = require('url'); var async = require('async'); var LZString = require('lz-string'); -var shortId = require('shortid'); var randomcolor = require("randomcolor"); var Chance = require('chance'), chance = new Chance(); var moment = require('moment'); //core -var config = require("../config.js"); +var config = require("./config.js"); var logger = require("./logger.js"); +var models = require("./models"); //ot var ot = require("./ot/index.js"); -//others -var db = require("./db.js"); -var Note = require("./note.js"); -var User = require("./user.js"); - //public var realtime = { io: null, @@ -72,12 +67,6 @@ function emitCheck(note) { lastchangeuserprofile: note.lastchangeuserprofile }; 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 @@ -88,70 +77,82 @@ var updater = setInterval(function () { async.each(Object.keys(notes), function (key, callback) { var note = notes[key]; if (note.server.isDirty) { - if (config.debug) - logger.info("updater found dirty note: " + key); - updaterUpdateMongo(note, function(err, result) { - if (err) return callback(err, null); - updaterUpdatePostgres(note, function(err, result) { - if (err) return callback(err, null); - callback(null, null); - }); + if (config.debug) logger.info("updater found dirty note: " + key); + updateNote(note, function(err, _note) { + if (!_note) { + realtime.io.to(note.id).emit('info', { + code: 404 + }); + 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 { - callback(null, null); + return callback(null, null); } }, function (err) { if (err) return logger.error('updater error', err); }); }, 1000); -function updaterUpdateMongo(note, callback) { - Note.findNote(note.id, function (err, _note) { - if (err || !_note) return callback(err, null); +function updateNote(note, callback) { + models.Note.findOne({ + where: { + id: note.id + } + }).then(function (_note) { + if (!_note) return callback(null, null); if (note.lastchangeuser) { - if (_note.lastchangeuser != note.lastchangeuser) { - var lastchangeuser = note.lastchangeuser; - var lastchangeuserprofile = null; - User.findUser(lastchangeuser, function (err, user) { - 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); - }); - } + if (_note.lastchangeuserId != note.lastchangeuser) { + models.User.findOne({ + where: { + id: note.lastchangeuser } + }).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 { - _note.lastchangeuser = null; note.lastchangeuserprofile = null; - Note.updateLastChangeUser(_note, null, function (err, result) { - if (err) return callback(err, null); - callback(null, null); - }); + return finishUpdateNote(note, _note, callback); } + }).catch(function (err) { + logger.error(err); + return callback(err, null); }); } -function updaterUpdatePostgres(note, callback) { - //postgres update +function finishUpdateNote(note, _note, callback) { var body = note.server.document; - var title = Note.getNoteTitle(body); + var title = models.Note.parseNoteTitle(body); title = LZString.compressToBase64(title); body = LZString.compressToBase64(body); - db.saveToDB(note.id, title, body, function (err, result) { - if (err) return callback(err, null); - note.server.isDirty = false; - note.updatetime = Date.now(); - emitCheck(note); - callback(null, null); + var values = { + title: title, + content: body, + lastchangeuserId: note.lastchangeuser, + lastchangeAt: Date.now() + }; + _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 @@ -170,15 +171,14 @@ var cleaner = setInterval(function () { disconnectSocketQueue.push(socket); disconnect(socket); } - callback(null, null); + return callback(null, null); }, function (err) { if (err) return logger.error('cleaner error', err); }); }, 60000); function getStatus(callback) { - db.countFromDB(function (err, data) { - if (err) return logger.info(err); + models.Note.count().then(function (notecount) { var distinctaddresses = []; var regaddresses = []; var distinctregaddresses = []; @@ -208,58 +208,58 @@ function getStatus(callback) { } } }); - User.getUserCount(function (err, regcount) { - if (err) { - logger.error('get status failed: ' + err); - return; - } - if (callback) - callback({ - onlineNotes: Object.keys(notes).length, - onlineUsers: Object.keys(users).length, - distinctOnlineUsers: distinctaddresses.length, - notesCount: data.rows[0].count, - registeredUsers: regcount, - onlineRegisteredUsers: regaddresses.length, - distinctOnlineRegisteredUsers: distinctregaddresses.length, - isConnectionBusy: isConnectionBusy, - connectionSocketQueueLength: connectionSocketQueue.length, - isDisconnectBusy: isDisconnectBusy, - disconnectSocketQueueLength: disconnectSocketQueue.length - }); + models.User.count().then(function (regcount) { + return callback ? callback({ + onlineNotes: Object.keys(notes).length, + onlineUsers: Object.keys(users).length, + distinctOnlineUsers: distinctaddresses.length, + notesCount: notecount, + registeredUsers: regcount, + onlineRegisteredUsers: regaddresses.length, + distinctOnlineRegisteredUsers: distinctregaddresses.length, + isConnectionBusy: isConnectionBusy, + connectionSocketQueueLength: connectionSocketQueue.length, + isDisconnectBusy: isDisconnectBusy, + disconnectSocketQueueLength: disconnectSocketQueue.length + }) : null; + }).catch(function (err) { + return logger.error('count user failed: ' + err); }); + }).catch(function (err) { + return logger.error('count note failed: ' + err); }); } -function getNotenameFromSocket(socket) { +function extractNoteIdFromSocket(socket) { if (!socket || !socket.handshake || !socket.handshake.headers) { - return; + return false; } var referer = socket.handshake.headers.referer; if (!referer) { - return socket.disconnect(true); + return false; } 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]; - if (notename == config.featuresnotename) { - return notename; - } - if (!Note.checkNoteIdValid(notename)) { - socket.emit('info', { - code: 404 - }); - return socket.disconnect(true); + var noteId = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1]; + return noteId; +} + +function parseNoteIdFromSocket(socket, callback) { + var noteId = extractNoteIdFromSocket(socket); + if (!noteId) { + return callback(null, null); } - notename = LZString.decompressFromBase64(notename); - return notename; + models.Note.parseNoteId(noteId, function (err, id) { + if (err || !id) return callback(err, id); + return callback(null, id); + }); } function emitOnlineUsers(socket) { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; var users = []; - Object.keys(notes[notename].users).forEach(function (key) { - var user = notes[notename].users[key]; + Object.keys(notes[noteId].users).forEach(function (key) { + var user = notes[noteId].users[key]; if (user) users.push(buildUserOutData(user)); }); @@ -267,35 +267,20 @@ function emitOnlineUsers(socket) { users: users }; out = LZString.compressToUTF16(JSON.stringify(out)); - realtime.io.to(notename).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); - }; - */ + realtime.io.to(noteId).emit('online users', out); } function emitUserStatus(socket) { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; var out = buildUserOutData(users[socket.id]); - socket.broadcast.to(notename).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); - } - }; - */ + socket.broadcast.to(noteId).emit('user status', out); } function emitRefresh(socket) { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; - var note = notes[notename]; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; + var note = notes[noteId]; socket.emit('refresh', { docmaxlength: config.documentmaxlength, owner: note.owner, @@ -326,15 +311,10 @@ function finishConnection(socket, note, user) { if (!socket || !note || !user) return; //check view permission 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 } else { - socket.emit('info', { - code: 403 - }); - clearSocketQueue(connectionSocketQueue, socket); - isConnectionBusy = false; - return socket.disconnect(true); + return failConnection(403, 'connection forbidden', socket); } } note.users[socket.id] = user; @@ -354,8 +334,8 @@ function finishConnection(socket, note, user) { startConnection(connectionSocketQueue[0]); if (config.debug) { - var notename = getNotenameFromSocket(socket); - logger.info('SERVER connected a client to [' + notename + ']:'); + var noteId = socket.noteId; + logger.info('SERVER connected a client to [' + noteId + ']:'); logger.info(JSON.stringify(user)); //logger.info(notes); getStatus(function (data) { @@ -367,117 +347,76 @@ function finishConnection(socket, note, user) { function startConnection(socket) { if (isConnectionBusy) return; isConnectionBusy = true; + + var noteId = socket.noteId; + if (!noteId) { + return failConnection(404, 'note id not found', socket); + } - var notename = getNotenameFromSocket(socket); - if (!notename) { - clearSocketQueue(connectionSocketQueue, socket); - isConnectionBusy = false; - return; - } - - if (!notes[notename]) { - db.readFromDB(notename, function (err, data) { - if (err) { - socket.emit('info', { - code: 404 - }); - socket.disconnect(true); - //clear err socket in queue - clearSocketQueue(connectionSocketQueue, socket); - isConnectionBusy = false; - return logger.error(err); + if (!notes[noteId]) { + var include = [{ + model: models.User, + as: "owner" + }, { + model: models.User, + as: "lastchangeuser" + }]; + + models.Note.findOne({ + where: { + id: noteId + }, + include: include + }).then(function (note) { + if (!note) { + return failConnection(404, 'note not found', socket); } - - var owner = data.rows[0].owner; - var ownerprofile = null; - - //find or new note - Note.findOrNewNote(notename, owner, function (err, note) { - if (err) { - socket.emit('info', { - code: 404 - }); - socket.disconnect(true); - clearSocketQueue(connectionSocketQueue, socket); - isConnectionBusy = false; - return logger.error(err); - } - - var body = LZString.decompressFromBase64(data.rows[0].content); - //body = LZString.compressToUTF16(body); - var createtime = data.rows[0].create_time; - var updatetime = data.rows[0].update_time; - var server = new ot.EditorSocketIOServer(body, [], notename, ifMayEdit); - - var lastchangeuser = note.lastchangeuser || null; - var lastchangeuserprofile = null; - - 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]); - }); - }); + var owner = note.ownerId; + var ownerprofile = note.owner ? models.User.parseProfile(note.owner.profile) : null; + + var lastchangeuser = note.lastchangeuserId; + var lastchangeuserprofile = note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null; + + var body = LZString.decompressFromBase64(note.content); + var createtime = note.createdAt; + var updatetime = note.lastchangeAt; + var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit); + + notes[noteId] = { + id: noteId, + owner: owner, + ownerprofile: ownerprofile, + permission: note.permission, + lastchangeuser: lastchangeuser, + lastchangeuserprofile: lastchangeuserprofile, + socks: [], + users: {}, + createtime: moment(createtime).valueOf(), + updatetime: moment(updatetime).valueOf(), + server: server + }; + + return finishConnection(socket, notes[noteId], users[socket.id]); + }).catch(function (err) { + return failConnection(500, err, socket); }); } 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) { if (isDisconnectBusy) return; isDisconnectBusy = true; @@ -490,8 +429,8 @@ function disconnect(socket) { if (users[socket.id]) { delete users[socket.id]; } - var notename = getNotenameFromSocket(socket); - var note = notes[notename]; + var noteId = socket.noteId; + var note = notes[noteId]; if (note) { delete note.users[socket.id]; do { @@ -502,22 +441,18 @@ function disconnect(socket) { } while (index != -1); if (Object.keys(note.users).length <= 0) { if (note.server.isDirty) { - var body = note.server.document; - var title = Note.getNoteTitle(body); - title = LZString.compressToBase64(title); - body = LZString.compressToBase64(body); - db.saveToDB(notename, title, body, - function (err, result) { - delete notes[notename]; - if (config.debug) { - //logger.info(notes); - getStatus(function (data) { - logger.info(JSON.stringify(data)); - }); - } - }); + updateNote(note, function (err, _note) { + if (err) return logger.error('disconnect note failed: ' + err); + delete notes[noteId]; + if (config.debug) { + //logger.info(notes); + getStatus(function (data) { + logger.info(JSON.stringify(data)); + }); + } + }); } else { - delete notes[notename]; + delete notes[noteId]; } } } @@ -556,10 +491,10 @@ function buildUserOutData(user) { function updateUserData(socket, user) { //retrieve user data from passport if (socket.request.user && socket.request.user.logged_in) { - var profile = JSON.parse(socket.request.user.profile); - user.photo = User.parsePhotoByProfile(profile); - user.name = profile.displayName || profile.username; - user.userid = socket.request.user._id; + var profile = models.User.parseProfile(socket.request.user.profile); + user.photo = profile.photo; + user.name = profile.name; + user.userid = socket.request.user.id; user.login = true; } else { user.userid = null; @@ -569,9 +504,9 @@ function updateUserData(socket, user) { } function ifMayEdit(socket, callback) { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; - var note = notes[notename]; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; + var note = notes[noteId]; var mayEdit = true; switch (note.permission) { case "freely": @@ -584,69 +519,78 @@ function ifMayEdit(socket, callback) { break; case "locked": case "private": //only owner can change - if (note.owner != socket.request.user._id) + if (note.owner != socket.request.user.id) mayEdit = false; break; } //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 if (socket.request.user && socket.request.user.logged_in) { - note.lastchangeuser = socket.request.user._id; + note.lastchangeuser = socket.request.user.id; } else { note.lastchangeuser = null; } } - callback(mayEdit); + return callback(mayEdit); } function connection(socket) { - //split notename from socket - var notename = getNotenameFromSocket(socket); - - //initialize user data - //random color - var color = randomcolor({ - luminosity: 'light' - }); - //make sure color not duplicated or reach max random count - if (notename && notes[notename]) { - var randomcount = 0; - var maxrandomcount = 5; - var found = false; - do { - Object.keys(notes[notename].users).forEach(function (user) { - if (user.color == color) { - found = true; - return; - } - }); - if (found) { - color = randomcolor({ - luminosity: 'light' + parseNoteIdFromSocket(socket, function (err, noteId) { + if (err) { + return failConnection(500, err, socket); + } + if (!noteId) { + return failConnection(404, 'note id not found', socket); + } + + // store noteId in this socket session + socket.noteId = noteId; + + //initialize user data + //random color + var color = randomcolor({ + luminosity: 'light' + }); + //make sure color not duplicated or reach max random count + if (notes[noteId]) { + var randomcount = 0; + var maxrandomcount = 5; + var found = false; + do { + Object.keys(notes[noteId].users).forEach(function (user) { + if (user.color == color) { + found = true; + return; + } }); - randomcount++; - } - } while (found && randomcount < maxrandomcount); - } - //create user data - users[socket.id] = { - id: socket.id, - address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address, - 'user-agent': socket.handshake.headers['user-agent'], - color: color, - cursor: null, - login: false, - userid: null, - name: null, - idle: false, - type: null - }; - updateUserData(socket, users[socket.id]); + if (found) { + color = randomcolor({ + luminosity: 'light' + }); + randomcount++; + } + } while (found && randomcount < maxrandomcount); + } + //create user data + users[socket.id] = { + id: socket.id, + address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address, + 'user-agent': socket.handshake.headers['user-agent'], + color: color, + cursor: null, + login: false, + userid: null, + name: null, + idle: false, + type: null + }; + updateUserData(socket, users[socket.id]); - //start connection - connectionSocketQueue.push(socket); - startConnection(socket); + //start connection + connectionSocketQueue.push(socket); + startConnection(socket); + }); //received client refresh request socket.on('refresh', function () { @@ -655,10 +599,10 @@ function connection(socket) { //received user status socket.on('user status', function (data) { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; 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) { var user = users[socket.id]; user.idle = data.idle; @@ -671,41 +615,44 @@ function connection(socket) { socket.on('permission', function (permission) { //need login to do more actions if (socket.request.user && socket.request.user.logged_in) { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; - var note = notes[notename]; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; + var note = notes[noteId]; //Only owner can change permission - if (note.owner == socket.request.user._id) { + if (note.owner == socket.request.user.id) { note.permission = permission; - Note.findNote(notename, function (err, _note) { - if (err || !_note) { + models.Note.update({ + permission: permission + }, { + where: { + id: noteId + } + }).then(function (count) { + if (!count) { return; } - Note.updatePermission(_note, permission, function (err, _note) { - if (err || !_note) { - return; - } - var out = { - permission: permission - }; - realtime.io.to(note.id).emit('permission', out); - for (var i = 0, l = note.socks.length; i < l; i++) { - var sock = note.socks[i]; - if (typeof sock !== 'undefined' && sock) { - //check view permission - if (permission == 'private') { - if (sock.request.user && sock.request.user.logged_in && sock.request.user._id == note.owner) { - //na - } else { - sock.emit('info', { - code: 403 - }); - return sock.disconnect(true); - } + var out = { + permission: permission + }; + realtime.io.to(note.id).emit('permission', out); + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i]; + if (typeof sock !== 'undefined' && sock) { + //check view permission + if (permission == 'private') { + if (sock.request.user && sock.request.user.logged_in && sock.request.user.id == note.owner) { + //na + } else { + 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 socket.on('user changed', function () { logger.info('user changed'); - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; - updateUserData(socket, notes[notename].users[socket.id]); + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; + updateUserData(socket, notes[noteId].users[socket.id]); emitOnlineUsers(socket); }); //received sync of online users request socket.on('online users', function () { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; var users = []; - Object.keys(notes[notename].users).forEach(function (key) { - var user = notes[notename].users[key]; + Object.keys(notes[noteId].users).forEach(function (key) { + var user = notes[noteId].users[key]; if (user) users.push(buildUserOutData(user)); }); @@ -744,55 +691,31 @@ function connection(socket) { //received cursor focus socket.on('cursor focus', function (data) { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; users[socket.id].cursor = data; var out = buildUserOutData(users[socket.id]); - socket.broadcast.to(notename).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); - } - }; - */ + socket.broadcast.to(noteId).emit('cursor focus', out); }); //received cursor activity socket.on('cursor activity', function (data) { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; users[socket.id].cursor = data; var out = buildUserOutData(users[socket.id]); - socket.broadcast.to(notename).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); - } - }; - */ + socket.broadcast.to(noteId).emit('cursor activity', out); }); //received cursor blur socket.on('cursor blur', function () { - var notename = getNotenameFromSocket(socket); - if (!notename || !notes[notename]) return; + var noteId = socket.noteId; + if (!noteId || !notes[noteId]) return; users[socket.id].cursor = null; var out = { id: socket.id }; - socket.broadcast.to(notename).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); - } - }; - */ + socket.broadcast.to(noteId).emit('cursor blur', out); }); //when a new client disconnect diff --git a/lib/response.js b/lib/response.js index 4e5fac3b..7a75e234 100644 --- a/lib/response.js +++ b/lib/response.js @@ -3,7 +3,6 @@ var ejs = require('ejs'); var fs = require('fs'); var path = require('path'); -var uuid = require('node-uuid'); var markdownpdf = require("markdown-pdf"); var LZString = require('lz-string'); var S = require('string'); @@ -13,12 +12,9 @@ var querystring = require('querystring'); var request = require('request'); //core -var config = require("../config.js"); - -//others -var db = require("./db.js"); -var Note = require("./note.js"); -var User = require("./user.js"); +var config = require("./config.js"); +var logger = require("./logger.js"); +var models = require("./models"); //slides var md = require('reveal.js/plugin/markdown/markdown'); @@ -26,10 +22,7 @@ var Mustache = require('mustache'); //reveal.js var opts = { - userBasePath: process.cwd(), - 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(), + template: fs.readFileSync(config.slidepath).toString(), theme: 'css/theme/black.css', highlightTheme: 'zenburn', 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."); }, newNote: newNote, - showFeatures: showFeatures, showNote: showNote, showPublishNote: showPublishNote, showPublishSlide: showPublishSlide, @@ -67,8 +59,13 @@ function responseError(res, code, detail, msg) { 'Content-Type': 'text/html' }); var template = config.errorpath; - var content = ejs.render(fs.readFileSync(template, 'utf8'), { - url: config.getserverurl(), + var options = { + cache: !config.debug, + filename: template + }; + var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options); + var content = compiled({ + url: config.serverurl, title: code + ' ' + detail + ' ' + msg, cache: !config.debug, filename: template, @@ -86,193 +83,163 @@ function showIndex(req, res, next) { 'Content-Type': 'text/html' }); var template = config.indexpath; - var content = ejs.render(fs.readFileSync(template, 'utf8'), { - url: config.getserverurl(), - useCDN: config.usecdn + var options = { + cache: !config.debug, + 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.end(); } -function responseHackMD(res, noteId) { - db.readFromDB(noteId, function (err, data) { - if (err) { - return response.errorNotFound(res); - } - var notedata = data.rows[0]; - var body = LZString.decompressFromBase64(notedata.content); - var meta = null; - try { - meta = metaMarked(body).meta; - } catch(err) { - //na - } - var title = Note.decodeTitle(notedata.title); - title = Note.generateWebTitle(title); - var template = config.hackmdpath; - var options = { - cache: !config.debug, - filename: template - }; - var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options); - var html = compiled({ - url: config.getserverurl(), - title: title, - useCDN: config.usecdn, - 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); +function responseHackMD(res, note) { + var body = LZString.decompressFromBase64(note.content); + var meta = null; + try { + meta = metaMarked(body).meta; + } catch(err) { + //na + } + var title = models.Note.decodeTitle(note.title); + title = models.Note.generateWebTitle(title); + var template = config.hackmdpath; + var options = { + cache: !config.debug, + filename: template + }; + var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options); + var html = compiled({ + url: config.serverurl, + title: title, + useCDN: config.usecdn, + robots: (meta && meta.robots) || false, //default allow robots + facebook: config.facebook, + twitter: config.twitter, + github: config.github, + dropbox: config.dropbox, }); + 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) { - var newId = uuid.v4(); - var body = fs.readFileSync(config.defaultnotepath, 'utf8'); - body = LZString.compressToBase64(body); var owner = null; if (req.isAuthenticated()) { - owner = req.user._id; + owner = req.user.id; } - db.newToDB(newId, owner, body, function (err, result) { - if (err) { - return response.errorInternalError(res); - } - Note.newNote(newId, owner, function(err, result) { - if (err) { - return response.errorInternalError(res); - } - res.redirect(config.getserverurl() + "/" + LZString.compressToBase64(newId)); - }); + models.Note.create({ + ownerId: owner + }).then(function (note) { + return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id)); + }).catch(function (err) { + logger.error(err); + return response.errorInternalError(res); }); } -function showFeatures(req, res, next) { - db.readFromDB(config.featuresnotename, function (err, data) { - if (err) { - var body = fs.readFileSync(config.defaultfeaturespath, 'utf8'); - body = LZString.compressToBase64(body); - db.newToDB(config.featuresnotename, null, body, function (err, result) { - if (err) { - return response.errorInternalError(res); - } - responseHackMD(res, config.featuresnotename); - }); - } else { - responseHackMD(res, config.featuresnotename); - } - }); +function checkViewPermission(req, note) { + if (note.permission == 'private') { + if (!req.isAuthenticated() || note.ownerId != req.user.id) + return false; + else + return true; + } else { + return true; + } } -function showNote(req, res, next) { - var noteId = req.params.noteId; - if (noteId != config.featuresnotename) { - 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) { +function findNote(req, res, callback, include) { + 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); } - //check view permission - if (note.permission == 'private') { - if (!req.isAuthenticated() || notedata.owner != req.user._id) - return response.errorForbidden(res); + if (!checkViewPermission(req, note)) { + return response.errorForbidden(res); + } else { + return callback(note); } - responseHackMD(res, noteId); + }).catch(function (err) { + logger.error(err); + return response.errorInternalError(res); }); }); } +function showNote(req, res, next) { + findNote(req, res, function (note) { + return responseHackMD(res, note); + }); +} + function showPublishNote(req, res, next) { - var shortid = req.params.shortid; - if (shortId.isValid(shortid)) { - Note.findNote(shortid, function (err, note) { - if (err || !note) { + var include = [{ + model: models.User, + as: "owner" + }, { + model: models.User, + as: "lastchangeuser" + }]; + findNote(req, res, function (note) { + note.increment('viewcount').then(function (note) { + if (!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); - } - //increase note viewcount - Note.increaseViewCount(note, function (err, note) { - if (err || !note) { - return response.errorNotFound(res); - } - var body = LZString.decompressFromBase64(notedata.content); - var meta = null; - try { - meta = metaMarked(body).meta; - } catch(err) { - //na - } - var updatetime = notedata.update_time; - var text = S(body).escapeHTML().s; - var title = Note.decodeTitle(notedata.title); - title = Note.generateWebTitle(title); - var origin = config.getserverurl(); - var data = { - 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); - } - - }); - }); + var body = LZString.decompressFromBase64(note.content); + var meta = null; + try { + meta = metaMarked(body).meta; + } catch(err) { + //na + } + var createtime = note.createdAt; + var updatetime = note.lastchangeAt; + var text = S(body).escapeHTML().s; + var title = models.Note.decodeTitle(note.title); + title = models.Note.generateWebTitle(title); + var origin = config.serverurl; + var data = { + title: title, + viewcount: note.viewcount, + createtime: createtime, + updatetime: updatetime, + url: origin, + body: text, + useCDN: config.usecdn, + lastchangeuserprofile: note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : null, + robots: (meta && meta.robots) || false //default allow robots + }; + return renderPublish(data, res); + }).catch(function (err) { + logger.error(err); + return response.errorInternalError(res); }); - } else { - return response.errorNotFound(res); - } + }, include); } function renderPublish(data, res) { var template = config.prettypath; var options = { - url: config.getserverurl(), + url: config.serverurl, cache: !config.debug, filename: template }; @@ -287,343 +254,206 @@ function renderPublish(data, res) { res.end(buf); } -function actionPublish(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); - } - res.redirect(config.getserverurl() + "/s/" + note.shortid); - }); - }); +function actionPublish(req, res, note) { + res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid)); } -function actionSlide(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); - } - res.redirect(config.getserverurl() + "/p/" + note.shortid); - }); - }); +function actionSlide(req, res, note) { + res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid)); } -function actionDownload(req, res, noteId) { - db.readFromDB(noteId, function (err, data) { - if (err) { - return response.errorNotFound(res); - } - var notedata = data.rows[0]; - var body = LZString.decompressFromBase64(notedata.content); - var title = Note.decodeTitle(notedata.title); - var filename = title; - filename = encodeURIComponent(filename); - res.writeHead(200, { - 'Access-Control-Allow-Origin': '*', //allow CORS as API - '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 actionDownload(req, res, note) { + var body = LZString.decompressFromBase64(note.content); + var title = models.Note.decodeTitle(note.title); + var filename = title; + filename = encodeURIComponent(filename); + res.writeHead(200, { + 'Access-Control-Allow-Origin': '*', //allow CORS as API + '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, noteId) { - db.readFromDB(noteId, function (err, data) { - if (err) { - return response.errorNotFound(res); - } - var notedata = data.rows[0]; - var body = LZString.decompressFromBase64(notedata.content); - try { - body = metaMarked(body).markdown; - } catch(err) { - //na - } - var title = Note.decodeTitle(notedata.title); +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; - // 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); - }); + 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 actionGist(req, res, note) { + var data = { + client_id: config.github.clientID, + redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/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) { var noteId = req.params.noteId; - if (noteId != config.featuresnotename) { - if (!Note.checkNoteIdValid(noteId)) { - return response.errorNotFound(res); + findNote(req, res, function (note) { + var action = req.params.action; + 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) { - var shortid = req.params.shortid; - if (shortId.isValid(shortid)) { - Note.findNote(shortid, 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 "edit": - if (note.id != config.featuresnotename) - res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(note.id)); - else - res.redirect(config.getserverurl() + '/' + note.id); - break; - } - }); - }); - } + findNote(req, res, function (note) { + var action = req.params.action; + switch (action) { + case "edit": + res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))); + break; + default: + res.redirect(config.serverurl + '/s/' + note.shortid); + break; + } + }); } function githubActions(req, res, next) { var noteId = req.params.noteId; - if (noteId != config.featuresnotename) { - if (!Note.checkNoteIdValid(noteId)) { - return response.errorNotFound(res); - } - noteId = LZString.decompressFromBase64(noteId); - if (!noteId) { - return response.errorNotFound(res); + findNote(req, res, function (note) { + var action = req.params.action; + switch (action) { + case "gist": + githubActionGist(req, res, note); + break; + default: + res.redirect(config.serverurl + '/' + noteId); + break; } - } - 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) { - db.readFromDB(noteId, function (err, data) { - if (err) { - return response.errorNotFound(res); +function githubActionGist(req, res, note) { + var code = req.query.code; + var state = req.query.state; + 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 code = req.query.code; - var state = req.query.state; - 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 auth_url = 'https://github.com/login/oauth/access_token'; - request({ - url: auth_url, - method: "POST", - json: data - }, function (error, httpResponse, body) { - if (!error && httpResponse.statusCode == 200) { - var access_token = body.access_token; - if (access_token) { - var content = LZString.decompressFromBase64(notedata.content); - var title = Note.decodeTitle(notedata.title); - var filename = title.replace('/', ' ') + '.md'; - var gist = { - "files": {} - }; - gist.files[filename] = { - "content": content - }; - var gist_url = "https://api.github.com/gists"; - request({ - url: gist_url, - 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); - } + var auth_url = 'https://github.com/login/oauth/access_token'; + request({ + url: auth_url, + method: "POST", + json: data + }, function (error, httpResponse, body) { + if (!error && httpResponse.statusCode == 200) { + var access_token = body.access_token; + if (access_token) { + var content = LZString.decompressFromBase64(note.content); + var title = models.Note.decodeTitle(note.title); + var filename = title.replace('/', ' ') + '.md'; + var gist = { + "files": {} + }; + gist.files[filename] = { + "content": content + }; + var gist_url = "https://api.github.com/gists"; + request({ + url: gist_url, + 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 { + return response.errorForbidden(res); + } + }) + } } function showPublishSlide(req, res, next) { - var shortid = req.params.shortid; - if (shortId.isValid(shortid)) { - Note.findNote(shortid, function (err, note) { - if (err || !note) { + findNote(req, res, function (note) { + note.increment('viewcount').then(function (note) { + if (!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); - } - //increase note viewcount - Note.increaseViewCount(note, function (err, note) { - if (err || !note) { - 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); - }); - }); + var body = LZString.decompressFromBase64(note.content); + try { + body = metaMarked(body).markdown; + } catch(err) { + //na + } + var title = models.Note.decodeTitle(note.title); + title = models.Note.generateWebTitle(title); + var text = S(body).escapeHTML().s; + render(res, title, text); + }).catch(function (err) { + logger.error(err); + return response.errorInternalError(res); }); - } else { - return response.errorNotFound(res); - } + }); } //reveal.js render @@ -631,7 +461,7 @@ var render = function (res, title, markdown) { var slides = md.slidify(markdown, opts); res.end(Mustache.to_html(opts.template, { - url: config.getserverurl(), + url: config.serverurl, title: title, theme: opts.theme, highlightTheme: opts.highlightTheme, diff --git a/lib/temp.js b/lib/temp.js deleted file mode 100644 index b635644d..00000000 --- a/lib/temp.js +++ /dev/null @@ -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;
\ No newline at end of file diff --git a/lib/user.js b/lib/user.js deleted file mode 100644 index 639d66c3..00000000 --- a/lib/user.js +++ /dev/null @@ -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;
\ No newline at end of file diff --git a/package.json b/package.json index 9619c1f7..2988c37d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "chance": "^1.0.1", "cheerio": "^0.20.0", "compression": "^1.6.1", - "connect-mongo": "^1.1.0", + "connect-session-sequelize": "^3.0.0", "cookie": "0.2.3", "cookie-parser": "1.4.1", "ejs": "^2.4.1", @@ -25,16 +25,15 @@ "highlight.js": "^9.2.0", "imgur": "^0.1.7", "jsdom-nogyp": "^0.8.3", - "kerberos": "0.0.19", "lz-string": "1.4.4", "markdown-pdf": "^7.0.0", "marked": "^0.3.5", "meta-marked": "^0.4.0", "method-override": "^2.3.5", "moment": "^2.12.0", - "mongoose": "^4.4.7", "morgan": "^1.7.0", "mustache": "2.2.1", + "mysql": "^2.10.2", "node-uuid": "^1.4.7", "passport": "^0.3.2", "passport-dropbox-oauth2": "^1.0.0", @@ -42,13 +41,17 @@ "passport-github": "^1.1.0", "passport-twitter": "^1.0.4", "passport.socketio": "^3.6.1", - "pg": "4.x", "randomcolor": "^0.4.3", "request": "^2.69.0", + "pg": "^4.5.3", + "pg-hstore": "^2.3.2", "reveal.js": "3.2.0", "shortid": "2.2.4", + "sequelize": "^3.21.0", "socket.io": "1.4.5", + "sqlite3": "^3.1.3", "string": "^3.3.1", + "tedious": "^1.14.0", "toobusy-js": "^0.4.3", "winston": "^2.2.0" }, diff --git a/public/features.md b/public/docs/features.md index dde852f0..a734f3c2 100644 --- a/public/features.md +++ b/public/docs/features.md @@ -3,7 +3,7 @@ Features 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**. 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. ## 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 -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: +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: ## Permission 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> ``` +## [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 === ## 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**. ###### 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 -- robots: set search engine to index or not +- robots: set web robots meta - lang: set browse language - 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 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 **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 **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 diff --git a/public/docs/slide-example.md b/public/docs/slide-example.md new file mode 100644 index 00000000..3d9a70c4 --- /dev/null +++ b/public/docs/slide-example.md @@ -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
\ No newline at end of file diff --git a/public/docs/yaml-metadata.md b/public/docs/yaml-metadata.md new file mode 100644 index 00000000..5fc6e1b8 --- /dev/null +++ b/public/docs/yaml-metadata.md @@ -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 +```
\ No newline at end of file diff --git a/public/index.ejs b/public/index.ejs deleted file mode 100644 index 87e86f2b..00000000 --- a/public/index.ejs +++ /dev/null @@ -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}} - <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>© 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">×</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">×</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>
\ No newline at end of file diff --git a/public/js/extra.js b/public/js/extra.js index 41b984dc..be454ed7 100644 --- a/public/js/extra.js +++ b/public/js/extra.js @@ -1,15 +1,24 @@ //auto update last change +var createtime = null; var lastchangetime = null; var lastchangeui = { + status: $(".ui-status-lastchange"), time: $(".ui-lastchange"), user: $(".ui-lastchangeuser"), nouser: $(".ui-no-lastchangeuser") } function updateLastChange() { - if (lastchangetime && lastchangeui) { - lastchangeui.time.html(moment(lastchangetime).fromNow()); - lastchangeui.time.attr('title', moment(lastchangetime).format('llll')); + if (!lastchangeui) return; + if (createtime) { + 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); diff --git a/public/js/history.js b/public/js/history.js index edecde1d..b3bae980 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -93,8 +93,14 @@ function clearDuplicatedHistory(notehistory) { for (var i = 0; i < notehistory.length; i++) { var found = false; for (var j = 0; j < newnotehistory.length; j++) { - var id = LZString.decompressFromBase64(notehistory[i].id); - var newId = LZString.decompressFromBase64(newnotehistory[j].id); + var id = notehistory[i].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) { 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'); diff --git a/public/js/index.js b/public/js/index.js index 6cbaf066..0e4fd21a 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1423,7 +1423,7 @@ function updatePermission(newPermission) { title = "Only owner can view & edit"; break; } - if (personalInfo.userid == owner) { + if (personalInfo.userid && personalInfo.userid == owner) { label += ' <i class="fa fa-caret-down"></i>'; ui.infobar.permission.label.removeClass('disabled'); } else { @@ -1476,11 +1476,14 @@ socket.emit = function () { socket.on('info', function (data) { console.error(data); switch (data.code) { + case 403: + location.href = "./403"; + break; case 404: location.href = "./404"; break; - case 403: - location.href = "./403"; + case 500: + location.href = "./500"; break; } }); @@ -1517,11 +1520,15 @@ socket.on('version', function (data) { }); function updateLastInfo(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; updateLastChange(); } - if (lastchangeuser !== data.lastchangeuser) { + if (data.hasOwnProperty('lastchangeuser') && lastchangeuser !== data.lastchangeuser) { lastchangeuser = data.lastchangeuser; lastchangeuserprofile = data.lastchangeuserprofile; updateLastChangeUser(); diff --git a/public/js/pretty.js b/public/js/pretty.js index 44d27e54..2d1f27de 100644 --- a/public/js/pretty.js +++ b/public/js/pretty.js @@ -20,7 +20,8 @@ renderTOC(markdown); generateToc('toc'); generateToc('toc-affix'); smoothHashScroll(); -lastchangetime = lastchangeui.time.text(); +createtime = lastchangeui.time.attr('data-createtime'); +lastchangetime = lastchangeui.time.attr('data-updatetime'); updateLastChange(); var url = window.location.pathname; $('.ui-edit').attr('href', url + '/edit'); diff --git a/public/views/body.ejs b/public/views/body.ejs index 54562ea6..044f7e6d 100644 --- a/public/views/body.ejs +++ b/public/views/body.ejs @@ -8,7 +8,7 @@ <span> <span class="ui-lastchangeuser" style="display: none;"> <i class="ui-user-icon small" data-toggle="tooltip" data-placement="right"></i></span> <span class="ui-no-lastchangeuser"> <i class="fa fa-clock-o"></i></span> - <span class="text-uppercase">changed</span> + <span class="text-uppercase ui-status-lastchange"></span> <span class="ui-lastchange text-uppercase"></span> </span> <span class="ui-permission dropdown pull-right"> @@ -73,32 +73,6 @@ </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">×</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 --> <div class="modal fade locked-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog modal-sm"> diff --git a/public/views/hackmd.ejs b/public/views/hackmd.ejs new file mode 100644 index 00000000..c5778fc9 --- /dev/null +++ b/public/views/hackmd.ejs @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <%- include head %> +</head> + +<body> + <%- include header %> + <%- include body %> + <%- include footer %> + <%- include foot %> +</body> + +</html>
\ No newline at end of file diff --git a/public/views/header.ejs b/public/views/header.ejs index 410886d1..bf8f9f66 100644 --- a/public/views/header.ejs +++ b/public/views/header.ejs @@ -38,8 +38,10 @@ </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> + <% 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> + <% } %> <li class="divider"></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> @@ -119,8 +121,10 @@ </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> + <% 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> + </li> + <% } %> <li class="divider"></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> diff --git a/public/views/index.ejs b/public/views/index.ejs index c5778fc9..85d955b7 100644 --- a/public/views/index.ejs +++ b/public/views/index.ejs @@ -2,14 +2,210 @@ <html lang="en"> <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> <body> - <%- include header %> - <%- include body %> - <%- include footer %> - <%- include foot %> + <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}} + <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>© 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">×</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> </html>
\ No newline at end of file diff --git a/public/views/modal.ejs b/public/views/modal.ejs new file mode 100644 index 00000000..260ff423 --- /dev/null +++ b/public/views/modal.ejs @@ -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">×</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>
\ No newline at end of file diff --git a/public/views/pretty.ejs b/public/views/pretty.ejs index ed6e638f..c8e959f4 100644 --- a/public/views/pretty.ejs +++ b/public/views/pretty.ejs @@ -49,8 +49,8 @@ <% } else { %> <span class="ui-no-lastchangeuser"> <i class="fa fa-clock-o"></i></span> <% } %> - <span class="text-uppercase">changed</span> - <span class="ui-lastchange text-uppercase"><%- updatetime %></span> + <span class="text-uppercase ui-status-lastchange"></span> + <span class="ui-lastchange text-uppercase" data-createtime="<%- createtime %>" data-updatetime="<%- updatetime %>"></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> </small> diff --git a/public/views/slide/reveal.hbs b/public/views/slide.hbs index 8a93c267..262a5df7 100644 --- a/public/views/slide/reveal.hbs +++ b/public/views/slide.hbs @@ -20,7 +20,7 @@ <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">' ); </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> <body> diff --git a/public/views/slide/listing.hbs b/public/views/slide/listing.hbs deleted file mode 100644 index 7da8ebab..00000000 --- a/public/views/slide/listing.hbs +++ /dev/null @@ -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> |