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 |

296
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", {
id: {
type: DataTypes.STRING,
primaryKey: true,
defaultValue: shortId.generate
},
data: {
type: DataTypes.TEXT
}
});
return Temp;
};

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

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

View file

@ -1,237 +0,0 @@
//note
//external modules
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var LZString = require('lz-string');
var marked = require('marked');
var cheerio = require('cheerio');
var shortId = require('shortid');
//others
var db = require("./db.js");
var logger = require("./logger.js");
//permission types
permissionTypes = ["freely", "editable", "locked", "private"];
// create a note model
var model = mongoose.model('note', {
id: String,
shortid: {
type: String,
unique: true,
default: shortId.generate
},
permission: {
type: String,
enum: permissionTypes
},
lastchangeuser: {
type: Schema.Types.ObjectId,
ref: 'user'
},
viewcount: {
type: Number,
default: 0
},
updated: Date,
created: Date
});
//public
var note = {
model: model,
findNote: findNote,
newNote: newNote,
findOrNewNote: findOrNewNote,
checkNoteIdValid: checkNoteIdValid,
checkNoteExist: checkNoteExist,
getNoteTitle: getNoteTitle,
decodeTitle: decodeTitle,
generateWebTitle: generateWebTitle,
increaseViewCount: increaseViewCount,
updatePermission: updatePermission,
updateLastChangeUser: updateLastChangeUser
};
function checkNoteIdValid(noteId) {
try {
//logger.info(noteId);
var id = LZString.decompressFromBase64(noteId);
if (!id) return false;
var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
var result = id.match(uuidRegex);
if (result && result.length == 1)
return true;
else
return false;
} catch (err) {
logger.error(err);
return false;
}
}
function checkNoteExist(noteId) {
try {
//logger.info(noteId);
var id = LZString.decompressFromBase64(noteId);
db.readFromDB(id, function (err, result) {
if (err) return false;
return true;
});
} catch (err) {
logger.error(err);
return false;
}
}
//get title
function getNoteTitle(body) {
var $ = cheerio.load(marked(body));
var h1s = $("h1");
var title = "";
if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
title = h1s.first().text();
else
title = "Untitled";
return title;
}
// decode title
function decodeTitle(title) {
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
else title = 'Untitled';
return title;
}
//generate note web page title
function generateWebTitle(title) {
title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD";
return title;
}
function findNote(id, callback) {
model.findOne({
$or: [
{
id: id
},
{
shortid: id
}
]
}, function (err, note) {
if (err) {
logger.error('find note failed: ' + err);
callback(err, null);
}
if (!err && note) {
callback(null, note);
} else {
logger.error('find note failed: ' + err);
callback(err, null);
};
});
}
function newNote(id, owner, callback) {
var permission = "freely";
if (owner && owner != "null") {
permission = "editable";
}
var note = new model({
id: id,
permission: permission,
updated: Date.now(),
created: Date.now()
});
note.save(function (err) {
if (err) {
logger.error('new note failed: ' + err);
callback(err, null);
} else {
logger.info("new note success: " + note.id);
callback(null, note);
};
});
}
function findOrNewNote(id, owner, callback) {
findNote(id, function (err, note) {
if (err || !note) {
newNote(id, owner, function (err, note) {
if (err) {
logger.error('find or new note failed: ' + err);
callback(err, null);
} else {
callback(null, note);
}
});
} else {
if (!note.permission) {
var permission = "freely";
if (owner && owner != "null") {
permission = "editable";
}
note.permission = permission;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('add note permission failed: ' + err);
callback(err, null);
} else {
logger.info("add note permission success: " + note.id);
callback(null, note);
};
});
} else {
callback(null, note);
}
}
});
}
function increaseViewCount(note, callback) {
note.viewcount++;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('increase note viewcount failed: ' + err);
callback(err, null);
} else {
logger.info("increase note viewcount success: " + note.id);
callback(null, note);
};
});
}
function updatePermission(note, permission, callback) {
note.permission = permission;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('update note permission failed: ' + err);
callback(err, null);
} else {
logger.info("update note permission success: " + note.id);
callback(null, note);
};
});
}
function updateLastChangeUser(note, lastchangeuser, callback) {
note.lastchangeuser = lastchangeuser;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('update note lastchangeuser failed: ' + err);
callback(err, null);
} else {
logger.info("update note lastchangeuser success: " + note.id);
callback(null, note);
};
});
}
module.exports = note;

View file

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

View file

@ -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;
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);
}
if (!Note.checkNoteIdValid(notename)) {
socket.emit('info', {
code: 404
});
return socket.disconnect(true);
}
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) {
@ -368,116 +348,75 @@ function startConnection(socket) {
if (isConnectionBusy) return;
isConnectionBusy = true;
var notename = getNotenameFromSocket(socket);
if (!notename) {
clearSocketQueue(connectionSocketQueue, socket);
isConnectionBusy = false;
return;
}
var noteId = socket.noteId;
if (!noteId) {
return failConnection(404, 'note id not found', socket);
}
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 = note.ownerId;
var ownerprofile = note.owner ? models.User.parseProfile(note.owner.profile) : null;
var owner = data.rows[0].owner;
var ownerprofile = null;
var lastchangeuser = note.lastchangeuserId;
var lastchangeuserprofile = note.lastchangeuser ? models.User.parseProfile(note.lastchangeuser.profile) : 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(note.content);
var createtime = note.createdAt;
var updatetime = note.lastchangeAt;
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit);
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);
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
};
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]);
});
});
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);
parseNoteIdFromSocket(socket, function (err, noteId) {
if (err) {
return failConnection(500, err, socket);
}
if (!noteId) {
return failConnection(404, 'note id not found', 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'
// 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

View file

@ -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 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);
}
if (!checkViewPermission(req, note)) {
return response.errorForbidden(res);
} else {
return callback(note);
}
}).catch(function (err) {
logger.error(err);
return response.errorInternalError(res);
});
});
}
function showNote(req, res, next) {
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) {
return response.errorNotFound(res);
}
//check view permission
if (note.permission == 'private') {
if (!req.isAuthenticated() || notedata.owner != req.user._id)
return response.errorForbidden(res);
}
responseHackMD(res, noteId);
});
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);
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, 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);
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);
// 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 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);
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);
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;
}
noteId = LZString.decompressFromBase64(noteId);
if (!noteId) {
return response.errorNotFound(res);
}
}
Note.findNote(noteId, function (err, note) {
if (err || !note) {
return response.errorNotFound(res);
}
db.readFromDB(note.id, function (err, data) {
if (err) {
return response.errorNotFound(res);
}
var notedata = data.rows[0];
//check view permission
if (note.permission == 'private') {
if (!req.isAuthenticated() || notedata.owner != req.user._id)
return response.errorForbidden(res);
}
var action = req.params.action;
switch (action) {
case "gist":
githubActionGist(req, res, noteId);
break;
default:
if (noteId != config.featuresnotename)
res.redirect(config.getserverurl() + '/' + LZString.compressToBase64(noteId));
else
res.redirect(config.getserverurl() + '/' + noteId);
break;
}
});
});
}
function githubActionGist(req, res, noteId) {
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,

View file

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

View file

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

View file

@ -13,7 +13,7 @@
"chance": "^1.0.1",
"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"
},

View file

@ -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

View file

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

View file

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

View file

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

View file

@ -1,15 +1,24 @@
//auto update last change
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);

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -38,8 +38,10 @@
</li>
<li 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>

View file

@ -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}}
&nbsp;<span class="label label-default">{{tag}}</span>
<div class="pull-right">
<i class="fa fa-clock-o"></i> {{date}}
</div>
</div>
</h5>
<hr>{{#each detail}}
<div class="text-left">
<h5><i class="fa fa-dot-circle-o"></i> {{title}}</h5>
<ul>
{{#each item}}
<li>
{{this}}
</li>
{{/each}}
</ul>
</div>
{{/each}}
</div>
{{#unless @last}}
<br>{{/unless}} {{/each}}
</div>
</div>
<div class="mastfoot">
<div class="inner">
<h6>
<iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="90px" height="20px" style="vertical-align:middle;"></iframe>
</h6>
<p>&copy; 2016 <a href="https://www.facebook.com/TakeHackMD" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> by <a href="https://github.com/jackycute" target="_blank"><i class="fa fa-github-square"></i> jackycute</a>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- delete modal -->
<div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="myModalLabel">Are you sure?</h4>
</div>
<div class="modal-body" style="color:black;">
<h5 class="ui-delete-modal-msg"></h5>
<strong class="ui-delete-modal-item"></strong>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger ui-delete-modal-confirm">Yes, do it!</button>
</div>
</div>
</div>
</div>
<%- include modal %>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<% if(useCDN) { %>
<script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/jquery.gsap.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.12.0/moment-with-locales.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.5/handlebars.min.js" defer></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/js-url/2.0.2/url.min.js" defer></script>
<% } else { %>
<script src="<%- url %>/vendor/jquery/dist/jquery.min.js" defer></script>
<script src="<%- url %>/vendor/bootstrap/dist/js/bootstrap.min.js" defer></script>
<script src="<%- url %>/vendor/gsap/src/minified/TweenMax.min.js" defer></script>
<script src="<%- url %>/vendor/gsap/src/minified/jquery.gsap.min.js" defer></script>
<script src="<%- url %>/vendor/select2/select2.min.js" defer></script>
<script src="<%- url %>/vendor/moment/min/moment-with-locales.min.js" defer></script>
<script src="<%- url %>/vendor/handlebars/handlebars.min.js" defer></script>
<script src="<%- url %>/vendor/js-url/url.min.js" defer></script>
<% } %>
<script src="<%- url %>/vendor/js.cookie.js" defer></script>
<script src="<%- url %>/vendor/list.min.js" defer></script>
<script src="<%- url %>/vendor/FileSaver.min.js" defer></script>
<script src="<%- url %>/vendor/store.min.js" defer></script>
<script src="<%- url %>/vendor/lz-string/libs/lz-string.min.js" defer></script>
<script src="<%- url %>/js/common.js" defer></script>
<script src="<%- url %>/js/history.js" defer></script>
<script src="<%- url %>/js/cover.js" defer></script>
</body>
</html>

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

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

View file

@ -49,8 +49,8 @@
<% } else { %>
<span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o"></i></span>
<% } %>
&nbsp;<span class="text-uppercase">changed</span>
<span class="ui-lastchange text-uppercase"><%- updatetime %></span>
&nbsp;<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>

View file

@ -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>

View file

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