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

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

View file

@ -9,6 +9,17 @@ Still in early stage, feel free to fork or contribute to this.
Thanks for your using! :smile:
[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>` |
| 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-side config.js settings
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 |

298
app.js
View file

@ -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);
});
});

View file

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

43
config.json Normal file
View file

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

View file

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

View file

@ -7,44 +7,60 @@ var GithubStrategy = require('passport-github').Strategy;
var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
//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));
if (config.dropbox) {
passport.use(new DropboxStrategy({
clientID: config.dropbox.clientID,
clientSecret: config.dropbox.clientSecret,
callbackURL: config.serverurl + '/auth/dropbox/callback'
}, callback));
}

112
lib/config.js Normal file
View file

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

151
lib/db.js
View file

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

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

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

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

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

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

@ -0,0 +1,19 @@
"use strict";
//external modules
var shortId = require('shortid');
module.exports = function (sequelize, DataTypes) {
var Temp = sequelize.define("Temp", {