diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..133695d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.gitignore +.dockerignore +test +CloudronManifest.json +README.md +DESCRIPTION.md +screenshots/* + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d570088 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ + diff --git a/CHANGELOG b/CHANGELOG index fd48cc2..694e2cd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,12 @@ -[0.0.2] -minor bugfix +[0.1.0] +* Initial version +* Made possible by @dictcp, @felix and @jeau -[0.0.1] -initial release +[0.2.0] +* Update screenshot and add documentation link + +[0.3.0] +* HackMD is now called CodiMD + +[0.3.1] +* Update to CodiMD 1.2.1 diff --git a/CloudronManifest.json b/CloudronManifest.json index 56a51dd..e776c10 100644 --- a/CloudronManifest.json +++ b/CloudronManifest.json @@ -1,11 +1,11 @@ { - "id": "io.hackmd", - "title": "HackMD", - "author": "HackMD authors", + "id": "io.hackmd.cloudronapp", + "title": "CodiMD", + "author": "CodiMD authors", "description": "file://DESCRIPTION.md", "changelog": "file://CHANGELOG", "tagline": "Best way to write and share your knowledge in markdown", - "version": "0.0.2", + "version": "0.3.1", "healthCheckPath": "/status", "httpPort": 3000, "addons": { @@ -14,15 +14,19 @@ "ldap": {} }, "manifestVersion": 1, - "website": "https://hackmd.io", + "website": "https://hackmd-ce.herokuapp.com/", "contactEmail": "support@cloudron.io", "icon": "logo.png", "tags": [ "markdown", "wiki", "document", - "collaboration" + "collaboration", + "notes" ], - "mediaLinks": [ "https://hackmd.io/screenshot.png" ] + "mediaLinks": [ + "https://s3.amazonaws.com/cloudron-app-screenshots/io.hackmd.cloudronapp/f41873c2e6538e0d246d7ad7111a4b4e8df15a5a/screenshot.png" + ], + "documentationUrl": "https://cloudron.io/documentation/apps/hackmd/" } diff --git a/DESCRIPTION.md b/DESCRIPTION.md index 48ec828..4218b38 100644 --- a/DESCRIPTION.md +++ b/DESCRIPTION.md @@ -1,6 +1,16 @@ -This app packages HackMD 0.5.2-snapshot (d1d6d58) +This app packages CodiMD 1.2.1 -HackMD lets you create realtime collaborative markdown notes on all -platforms. -Inspired by Hackpad, with more focus on speed and flexibility. +CodiMD lets you create realtime collaborative markdown notes on all +platforms. Inspired by Hackpad, with more focus on speed and flexibility. + +## Features + +* Documentation Collaborated +* Context Captured +* Native Markdown +* Knowledge Net +* Technical Sharing and Presentation +* Turn Notes into Slides +* Better Conference Experience +* Questions Polling diff --git a/Dockerfile b/Dockerfile index fcfbf1c..587c4d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,33 +1,37 @@ FROM cloudron/base:0.10.0 +# install jq and moreutils (for sponge). +# can be removed once https://git.cloudron.io/cloudron/docker-base-image/merge_requests/3 is merged and released +RUN apt update && \ + apt -y install moreutils jq && \ + rm -rf /var/cache/apt /var/lib/apt/lists # setup nodejs version -ENV NODEJS_VERSION 6.9.5 -RUN ln -s /usr/local/node-$NODEJS_VERSION/bin/node /usr/local/bin/node && \ - ln -s /usr/local/node-$NODEJS_VERSION/bin/npm /usr/local/bin/npm +RUN mkdir -p /usr/local/node-8.11.3 +RUN curl -L https://nodejs.org/download/release/v8.11.3/node-v8.11.3-linux-x64.tar.gz | tar zxf - --strip-components 1 -C /usr/local/node-8.11.3 +ENV PATH /usr/local/node-8.11.3/bin:$PATH -WORKDIR /hackmd +WORKDIR /app/code -ENV HACKMD_VERSION d1d6d5810b12645ddb02275ce0c2498b2189a8a0 -RUN curl -L https://github.com/hackmdio/hackmd/archive/$HACKMD_VERSION.tar.gz | tar -xz --strip-components 1 -f - +ENV HACKMD_VERSION 1.2.1 +RUN curl -L https://github.com/hackmdio/codimd/archive/$HACKMD_VERSION.tar.gz | tar -xz --strip-components 1 -f - # npm, deps -RUN npm install +RUN npm install && npm run build -# build front-end bundle -RUN npm run build - -# remove dev dependencies -RUN npm prune --production +# generate sequelizerrc +RUN sed -e "s/'change this'/process.env.POSTGRESQL_URL/" /app/code/.sequelizerc.example > /app/code/.sequelizerc # add utils -ADD CloudronManifest.json ./ -ADD start.sh ./ -RUN chmod +x ./start.sh +ADD start.sh /app/code + +# constant.js is generated on startup and "require"d by the code +RUN ln -sfn /run/hackmd/constant.js /app/code/public/build/constant.js && \ + rm -rf /app/code/public/uploads && ln -sf /app/data/uploads /app/code/public/uploads -# use local storage -RUN ln -sfn /app/data/build/constant.js ./public/build/constant.js && \ - rm -rf ./public/uploads && ln -sf /app/data/uploads ./public/uploads +# add user definable config +ADD config.json /app/code/config.json-cloudron +RUN ln -sfn /app/data/config.json /app/code/config.json EXPOSE 3000 -CMD ["/hackmd/start.sh"] +CMD ["/app/code/start.sh"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1d8e98b --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6b31c0 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +# HackMD Cloudron App + +This repository contains the Cloudron app package source for [HackMD](https://hackmd.io/). + +## Installation + +[![Install](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=io.hackmd.cloudronapp) + +or using the [Cloudron command line tooling](https://cloudron.io/references/cli.html) + +``` +cloudron install --appstore-id io.hackmd.cloudronapp +``` + +## Building + +The app package can be built using the [Cloudron command line tooling](https://cloudron.io/references/cli.html). + +``` +cd hackmd-app + +cloudron build +cloudron install +``` + +## Testing + +The e2e tests are located in the `test/` folder and require [nodejs](http://nodejs.org/). They are creating a fresh build, install the app on your Cloudron, backup and restore. + +``` +cd hackmd-app/test + +npm install +USERNAME= PASSWORD= mocha --bail test.js +``` + diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0884765 --- /dev/null +++ b/TODO.md @@ -0,0 +1,3 @@ +# TODO list + +- merge cloudron docker image with upstream source from https://github.com/hackmdio/docker-hackmd diff --git a/config.json b/config.json new file mode 100644 index 0000000..8c54178 --- /dev/null +++ b/config.json @@ -0,0 +1,12 @@ +{ + "production": { + "allowAnonymous": false, + "allowAnonymousEdits": true, + "allowEmailRegister": false, + "allowFreeUrl": true, + "allowPdfExport": false, + "debug": false, + "defaultPermission": "private", + "email": false + } +} diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index 37c2e8d..5bea530 --- a/start.sh +++ b/start.sh @@ -1,29 +1,43 @@ #!/bin/bash +set -eu + # prepare data directory -mkdir -p /app/data/build && \ -mkdir -p /app/data/uploads -chown -R cloudron:cloudron /app/data +mkdir -p /app/data/uploads /tmp/hackmd /run/hackmd -if [ -f .sequelizerc ]; -then - node_modules/.bin/sequelize db:migrate +if [ ! -e /app/data/config.json ]; then + echo "Creating initial template on first run" + cp /app/code/config.json-cloudron /app/data/config.json fi -# wait for db up -sleep 3 +# generate and store an unique sessionSecret for this installation +CONFIG_JSON=/app/data/config.json +if [ $(jq .production.sessionSecret $CONFIG_JSON) == "null" ]; then + echo "generating sessionSecret" + sessionsecret=$(pwgen -1sc 32) + jq ".production.sessionSecret = \"$sessionsecret\"" $CONFIG_JSON | sponge $CONFIG_JSON +fi -export NODE_ENV='production' -export HMD_ALLOW_ANONYMOUS="false" +# these cannot be changed by user (https://github.com/hackmdio/hackmd/wiki/Environment-Variables) +export HMD_DOMAIN="$APP_DOMAIN" +export HMD_PROTOCOL_USESSL=true export HMD_DB_URL="$POSTGRESQL_URL" export HMD_LDAP_URL="$LDAP_URL" export HMD_LDAP_BINDDN="$LDAP_BIND_DN" export HMD_LDAP_BINDCREDENTIALS="$LDAP_BIND_PASSWORD" export HMD_LDAP_SEARCHBASE="$LDAP_USERS_BASE_DN" -export HMD_LDAP_SEARCHFILTER="(username={{username}})" -export HMD_EMAIL=false -export HMD_ALLOW_EMAIL_REGISTER=false +export HMD_LDAP_SEARCHFILTER="(|(username={{username}})(mail={{username}}))" +export HMD_LDAP_USERNAMEFIELD="username" +export HMD_PORT=3000 export HMD_IMAGE_UPLOAD_TYPE=filesystem +export HMD_TMP_PATH=/tmp/hackmd + +echo "Running db migrations" +node_modules/.bin/sequelize db:migrate + +chown -R cloudron:cloudron /app/data /tmp/hackmd /run/hackmd # run -/usr/local/bin/gosu cloudron:cloudron node app.js +export NODE_ENV=production +exec /usr/local/bin/gosu cloudron:cloudron node app.js + diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..d08f3cd --- /dev/null +++ b/test/package.json @@ -0,0 +1,24 @@ +{ + "name": "test", + "version": "1.0.0", + "description": "", + "main": "test.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "ejs": "^2.3.4", + "expect.js": "^0.3.1", + "mkdirp": "^0.5.1", + "mocha": "^2.3.4", + "rimraf": "^2.4.4", + "selenium-server-standalone-jar": "^2.53.0", + "selenium-webdriver": "^2.53.3", + "superagent": "^1.4.0" + }, + "dependencies": { + "chromedriver": "^2.37.0" + } +} diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..098bfa2 --- /dev/null +++ b/test/test.js @@ -0,0 +1,220 @@ +#!/usr/bin/env node + +/* jslint node:true */ +/* global it:false */ +/* global xit:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +require('chromedriver'); + +var execSync = require('child_process').execSync, + expect = require('expect.js'), + path = require('path'), + webdriver = require('selenium-webdriver'); + +var by = webdriver.By, + until = webdriver.until; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +if (!process.env.USERNAME || !process.env.PASSWORD || !process.env.EMAIL) { + console.log('USERNAME, PASSWORD and EMAIL env vars need to be set'); + process.exit(1); +} + +describe('Application life cycle test', function () { + this.timeout(0); + + var chrome = require('selenium-webdriver/chrome'); + var server, browser = new chrome.Driver(); + var username = process.env.USERNAME, password = process.env.PASSWORD; + var email = process.env.EMAIL; + var noteUrl; + + function login(username, done) { + browser.manage().deleteAllCookies().then(function () { + return browser.get('https://' + app.fqdn); + }).then(function () { + return browser.wait(until.elementLocated(by.xpath('//button[text()="Sign In"]')), TEST_TIMEOUT); + }).then(function () { + return browser.findElement(by.xpath('//div[@class="ui-signin"]/button[text()="Sign In"]')).click(); + }).then(function () { + return browser.sleep(2000); // wait for login popup + }).then(function () { + return browser.findElement(by.xpath('//input[@name="username"]')).sendKeys(username); + }).then(function () { + return browser.findElement(by.xpath('//input[@name="password"]')).sendKeys(password); + }).then(function () { + return browser.findElement(by.xpath('//button[text()="Sign in" and contains(@formaction, "ldap")]')).click(); + }).then(function () { + return browser.wait(until.elementLocated(by.xpath('//a[contains(text(), "New note")]')), TEST_TIMEOUT); + }).then(function () { + done(); + }); + } + + function newNote(done) { + browser.get('https://' + app.fqdn + '/new').then(function () { + return browser.wait(until.elementLocated(by.xpath('//a[contains(text(), "Publish")]')), TEST_TIMEOUT); + }).then(function () { + return browser.sleep(5000); // code mirror takes a while to load + }).then(function () { + return browser.getCurrentUrl(); + }).then(function (url) { + noteUrl = url; + console.log('The note url is ' + noteUrl); + return browser.findElement(by.css('.CodeMirror textarea')).sendKeys('hello cloudron'); + }).then(function () { + return browser.sleep(2000); // give it a second to 'save' + }).then(function () { + done(); + }); + } + + function checkExistingNote(done) { + browser.get(noteUrl).then(function () { + return browser.wait(until.elementLocated(by.xpath('//p[contains(text(), "hello cloudron")]')), TEST_TIMEOUT); + }).then(function () { + done(); + }); + } + + function checkNoteIsPrivate(done) { + browser.get(noteUrl).then(function () { + return browser.wait(until.elementLocated(by.xpath('//h1[contains(text(), "403 Forbidden")]')), TEST_TIMEOUT); + }).then(function () { + done(); + }); + } + + function logout(done) { + browser.get('https://' + app.fqdn).then(function () { + return browser.findElement(by.xpath('//button[@id="profileLabel"]')).click(); + }).then(function () { + return browser.sleep(2000); // wait for menu to open + }).then(function () { + return browser.findElement(by.xpath('//a[contains(text(), "Sign Out")]')).click(); + }).then(function () { + return browser.sleep(2000); + }).then(function () { + done(); + }); + } + + before(function (done) { + var seleniumJar= require('selenium-server-standalone-jar'); + var SeleniumServer = require('selenium-webdriver/remote').SeleniumServer; + server = new SeleniumServer(seleniumJar.path, { port: 4444 }); + server.start(); + + done(); + }); + + after(function (done) { + browser.quit(); + server.stop(); + done(); + }); + + var LOCATION = 'test'; + var TEST_TIMEOUT = 30000; + var app; + + xit('build app', function () { + execSync('cloudron build', { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + }); + + it('install app', function () { + execSync('cloudron install --new --wait --location ' + LOCATION, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + }); + + it('can get app information', function () { + var inspect = JSON.parse(execSync('cloudron inspect')); + + app = inspect.apps.filter(function (a) { return a.location === LOCATION; })[0]; + + expect(app).to.be.an('object'); + }); + + it('can login', login.bind(null, username)); + it('can create new note', newNote); + it('can check existing note', checkExistingNote); + it('can logout', logout); + + it('did create private note', checkNoteIsPrivate); + + it('backup app', function () { + execSync('cloudron backup create --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + }); + + it('restore app', function () { + execSync('cloudron restore --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + }); + + it('can login', login.bind(null, username)); + it('can check existing note', checkExistingNote); + it('can logout', logout); + + it('did create private note', checkNoteIsPrivate); + + it('can restart app', function (done) { + execSync('cloudron restart --wait --app ' + app.id); + done(); + }); + + it('can login', login.bind(null, email)); + it('can check existing note', checkExistingNote); + it('can logout', logout); + + it('did create private note', checkNoteIsPrivate); + + it('move to different location', function (done) { + execSync('cloudron configure --wait --location ' + LOCATION + '2 --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + var inspect = JSON.parse(execSync('cloudron inspect')); + app = inspect.apps.filter(function (a) { return a.location === LOCATION + '2'; })[0]; + expect(app).to.be.an('object'); + noteUrl = noteUrl.replace(LOCATION, LOCATION + '2'); + + done(); + }); + + it('can login', login.bind(null, username)); + it('can check existing note', checkExistingNote); + it('can logout', logout); + + it('did create private note', checkNoteIsPrivate); + + it('uninstall app', function () { + execSync('cloudron uninstall --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + }); + + // test update + it('can install from appstore', function () { + execSync('cloudron install --new --wait --appstore-id io.hackmd.cloudronapp --location ' + LOCATION, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + var inspect = JSON.parse(execSync('cloudron inspect')); + app = inspect.apps.filter(function (a) { return a.location === LOCATION; })[0]; + expect(app).to.be.an('object'); + }); + + it('can login', login.bind(null, username)); + it('can create new note', newNote); + it('can logout', logout); + + it('can update', function () { + execSync('cloudron install --wait --app ' + LOCATION, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + }); + + it('can login', login.bind(null, email)); + it('can check existing note', checkExistingNote); + it('can logout', logout); + + it('did create private note', checkNoteIsPrivate); + + it('uninstall app', function () { + execSync('cloudron uninstall --app ' + app.id, { cwd: path.resolve(__dirname, '..'), stdio: 'inherit' }); + }); +});