├── .babelrc ├── .buildpacks ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .mailmap ├── .nvmrc ├── .sequelizerc.example ├── .travis.yml ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README.md ├── app.js ├── app.json ├── bin ├── heroku ├── heroku_start.sh ├── manage_users └── setup ├── config.json.example ├── contribute └── developer-certificate-of-origin ├── deployments ├── Dockerfile ├── build.sh ├── docker-compose.yml └── docker-entrypoint.sh ├── docker-compose.yaml ├── docker-helper ├── Makefile ├── build │ └── Dockerfile └── runtime │ ├── Dockerfile │ └── local.conf ├── dockerfile ├── lib ├── auth │ ├── bitbucket │ │ └── index.js │ ├── dropbox │ │ └── index.js │ ├── email │ │ └── index.js │ ├── facebook │ │ └── index.js │ ├── github │ │ └── index.js │ ├── gitlab │ │ └── index.js │ ├── google │ │ └── index.js │ ├── index.js │ ├── ldap │ │ └── index.js │ ├── mattermost │ │ └── index.js │ ├── oauth2 │ │ ├── index.js │ │ └── strategy.js │ ├── openid │ │ └── index.js │ ├── saml │ │ └── index.js │ ├── twitter │ │ └── index.js │ └── utils.js ├── config │ ├── default.js │ ├── defaultSSL.js │ ├── dockerSecret.js │ ├── enum.js │ ├── environment.js │ ├── index.js │ └── utils.js ├── csp.js ├── errorPage │ └── index.js ├── history │ └── index.js ├── homepage │ └── index.js ├── imageRouter │ ├── azure.js │ ├── filesystem.js │ ├── imgur.js │ ├── index.js │ ├── lutim.js │ ├── minio.js │ └── s3.js ├── letter-avatars.js ├── logger.js ├── metrics.js ├── middleware │ ├── checkURIValid.js │ ├── codiMDVersion.js │ ├── redirectWithoutTrailingSlashes.js │ └── tooBusy.js ├── migrations │ ├── 20150504155329-create-users.js │ ├── 20150508114741-create-notes.js │ ├── 20150515125813-create-temp.js │ ├── 20150702001020-update-to-0_3_1.js │ ├── 20150915153700-change-notes-title-to-text.js │ ├── 20160112220142-note-add-lastchange.js │ ├── 20160420180355-note-add-alias.js │ ├── 20160515114000-user-add-tokens.js │ ├── 20160607060246-support-revision.js │ ├── 20160703062241-support-authorship.js │ ├── 20161009040430-support-delete-note.js │ ├── 20161201050312-support-email-signin.js │ ├── 20171009121200-longtext-for-mysql.js │ ├── 20180209120907-longtext-of-authorship.js │ ├── 20180306150303-fix-enum.js │ ├── 20180326103000-use-text-in-tokens.js │ ├── 20180525153000-user-add-delete-token.js │ └── 20200104215332-remove-temp-table.js ├── models │ ├── author.js │ ├── index.js │ ├── note.js │ ├── revision.js │ └── user.js ├── note │ ├── index.js │ └── noteActions.js ├── ot │ ├── client.js │ ├── editor-socketio-server.js │ ├── index.js │ ├── selection.js │ ├── server.js │ ├── simple-text-operation.js │ ├── text-operation.js │ └── wrapped-operation.js ├── realtime │ ├── processQueue.js │ ├── realtime.js │ ├── realtimeCleanDanglingUserJob.js │ ├── realtimeClientConnection.js │ ├── realtimeSaveRevisionJob.js │ └── realtimeUpdateDirtyNoteJob.js ├── response.js ├── routes.js ├── status │ └── index.js ├── user │ └── index.js ├── utils.js ├── web │ └── middleware │ │ └── checkVersion.js └── workers │ ├── dmpWorker.js │ └── liaWorker.js ├── locales ├── ca.json ├── da.json ├── de.json ├── el.json ├── en.json ├── eo.json ├── es.json ├── fr.json ├── hi.json ├── hr.json ├── id.json ├── it.json ├── ja.json ├── ko.json ├── nl.json ├── pl.json ├── pt.json ├── ru.json ├── sr.json ├── sv.json ├── tr.json ├── uk.json ├── zh-CN.json └── zh-TW.json ├── package-lock.json ├── package.json ├── public ├── .eslintrc.js ├── apple-touch-icon.png ├── codimd-icon-1024.png ├── css │ ├── bootstrap-social.css │ ├── center.css │ ├── codemirror-extend │ │ ├── ayu-dark.css │ │ ├── ayu-mirage.css │ │ ├── tomorrow-night-bright.css │ │ └── tomorrow-night-eighties.css │ ├── cover.css │ ├── extra.css │ ├── font.css │ ├── github-extract.css │ ├── google-font.css │ ├── index.css │ ├── markdown.css │ ├── mermaid.css │ ├── site.css │ ├── slide-preview.css │ └── slide.css ├── default.md ├── docs │ ├── features.md │ ├── privacy.md.example │ ├── release-notes.md │ ├── slide-example.md │ └── yaml-metadata.md ├── favicon.png ├── fonts │ ├── SourceCodePro-Black.eot │ ├── SourceCodePro-Black.ttf │ ├── SourceCodePro-Black.woff │ ├── SourceCodePro-Bold.eot │ ├── SourceCodePro-Bold.ttf │ ├── SourceCodePro-Bold.woff │ ├── SourceCodePro-ExtraLight.eot │ ├── SourceCodePro-ExtraLight.ttf │ ├── SourceCodePro-ExtraLight.woff │ ├── SourceCodePro-Light.eot │ ├── SourceCodePro-Light.ttf │ ├── SourceCodePro-Light.woff │ ├── SourceCodePro-Medium.eot │ ├── SourceCodePro-Medium.ttf │ ├── SourceCodePro-Medium.woff │ ├── SourceCodePro-Regular.eot │ ├── SourceCodePro-Regular.ttf │ ├── SourceCodePro-Regular.woff │ ├── SourceCodePro-Semibold.eot │ ├── SourceCodePro-Semibold.ttf │ ├── SourceCodePro-Semibold.woff │ ├── SourceSansPro-Black.eot │ ├── SourceSansPro-Black.ttf │ ├── SourceSansPro-Black.woff │ ├── SourceSansPro-BlackItalic.eot │ ├── SourceSansPro-BlackItalic.ttf │ ├── SourceSansPro-BlackItalic.woff │ ├── SourceSansPro-Bold.eot │ ├── SourceSansPro-Bold.ttf │ ├── SourceSansPro-Bold.woff │ ├── SourceSansPro-BoldItalic.eot │ ├── SourceSansPro-BoldItalic.ttf │ ├── SourceSansPro-BoldItalic.woff │ ├── SourceSansPro-ExtraLight.eot │ ├── SourceSansPro-ExtraLight.ttf │ ├── SourceSansPro-ExtraLight.woff │ ├── SourceSansPro-ExtraLightItalic.eot │ ├── SourceSansPro-ExtraLightItalic.ttf │ ├── SourceSansPro-ExtraLightItalic.woff │ ├── SourceSansPro-Italic.eot │ ├── SourceSansPro-Italic.ttf │ ├── SourceSansPro-Italic.woff │ ├── SourceSansPro-Light.eot │ ├── SourceSansPro-Light.ttf │ ├── SourceSansPro-Light.woff │ ├── SourceSansPro-LightItalic.eot │ ├── SourceSansPro-LightItalic.ttf │ ├── SourceSansPro-LightItalic.woff │ ├── SourceSansPro-Regular.eot │ ├── SourceSansPro-Regular.ttf │ ├── SourceSansPro-Regular.woff │ ├── SourceSansPro-Semibold.eot │ ├── SourceSansPro-Semibold.ttf │ ├── SourceSansPro-Semibold.woff │ ├── SourceSansPro-SemiboldItalic.eot │ ├── SourceSansPro-SemiboldItalic.ttf │ ├── SourceSansPro-SemiboldItalic.woff │ ├── SourceSerifPro-Bold.eot │ ├── SourceSerifPro-Bold.ttf │ ├── SourceSerifPro-Bold.woff │ ├── SourceSerifPro-Regular.eot │ ├── SourceSerifPro-Regular.ttf │ ├── SourceSerifPro-Regular.woff │ ├── SourceSerifPro-Semibold.eot │ ├── SourceSerifPro-Semibold.ttf │ └── SourceSerifPro-Semibold.woff ├── images │ └── mattermost-logo.svg ├── js │ ├── cover.js │ ├── extra.js │ ├── history.js │ ├── htmlExport.js │ ├── index.js │ ├── liaHelp.js │ ├── lib │ │ ├── appState.js │ │ ├── common │ │ │ ├── constant.ejs │ │ │ ├── login.js │ │ │ └── metrics.ejs │ │ ├── config │ │ │ └── index.js │ │ ├── editor │ │ │ ├── config.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ ├── markdown-lint │ │ │ │ └── index.js │ │ │ ├── spellcheck.js │ │ │ ├── statusbar.html │ │ │ ├── table-editor.js │ │ │ ├── toolbar.html │ │ │ ├── ui-elements.js │ │ │ └── utils.js │ │ ├── markdown │ │ │ └── utils.js │ │ ├── modeType.js │ │ └── renderer │ │ │ └── lightbox │ │ │ ├── index.js │ │ │ └── lightbox.css │ ├── locale.js │ ├── pretty.js │ ├── render.js │ ├── tutorial.js │ └── utils.js ├── markdown-lint │ ├── css │ │ └── lint.css │ └── images │ │ ├── mark-error.png │ │ ├── mark-multiple.png │ │ ├── mark-warning.png │ │ ├── message-error.png │ │ └── message-warning.png ├── screenshot.gif ├── screenshot.png ├── uploads │ └── .gitkeep ├── vendor │ ├── bootstrap │ │ ├── tooltip.min.css │ │ └── tooltip.min.js │ ├── codemirror-spell-checker │ │ ├── en_US.aff │ │ ├── en_US.dic │ │ └── spell-checker.min.css │ ├── inlineAttachment │ │ ├── codemirror.inline-attachment.js │ │ └── inline-attachment.js │ ├── jquery-textcomplete │ │ └── jquery.textcomplete.js │ ├── jquery-ui │ │ ├── images │ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ │ ├── ui-bg_glass_55_fbf9ee_1x400.png │ │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ │ ├── ui-bg_glass_75_dadada_1x400.png │ │ │ ├── ui-bg_glass_75_e6e6e6_1x400.png │ │ │ ├── ui-bg_glass_95_fef1ec_1x400.png │ │ │ ├── ui-bg_highlight-soft_75_cccccc_1x100.png │ │ │ ├── ui-icons_222222_256x240.png │ │ │ ├── ui-icons_2e83ff_256x240.png │ │ │ ├── ui-icons_454545_256x240.png │ │ │ ├── ui-icons_888888_256x240.png │ │ │ └── ui-icons_cd0a0a_256x240.png │ │ ├── jquery-ui.min.css │ │ └── jquery-ui.min.js │ ├── md-toc.js │ ├── ot │ │ ├── ajax-adapter.js │ │ ├── client.js │ │ ├── codemirror-adapter.js │ │ ├── compress.sh │ │ ├── editor-client.js │ │ ├── ot.min.js │ │ ├── selection.js │ │ ├── socketio-adapter.js │ │ ├── text-operation.js │ │ ├── undo-manager.js │ │ └── wrapped-operation.js │ └── showup │ │ ├── showup.css │ │ └── showup.js └── views │ ├── codimd.ejs │ ├── codimd │ ├── body.ejs │ ├── foot.ejs │ ├── footer.ejs │ ├── head.ejs │ └── header.ejs │ ├── error.ejs │ ├── html.hbs │ ├── includes │ ├── header.ejs │ └── scripts.ejs │ ├── index.ejs │ ├── index │ ├── body.ejs │ ├── foot.ejs │ ├── footer.ejs │ ├── head.ejs │ └── header.ejs │ ├── pretty.ejs │ ├── shared │ ├── disqus.ejs │ ├── ga.ejs │ ├── help-modal.ejs │ ├── pandoc-export-modal.ejs │ ├── polyfill.ejs │ ├── refresh-modal.ejs │ ├── revision-modal.ejs │ └── signin-modal.ejs │ └── slide.ejs ├── scalingo.json ├── test ├── auth │ └── oauth2 │ │ └── strategy.test.js ├── connectionQueue.test.js ├── csp.js ├── letter-avatars.js ├── realtime │ ├── cleanDanglingUser.test.js │ ├── connection.test.js │ ├── dirtyNoteUpdate.test.js │ ├── disconnect-process.test.js │ ├── extractNoteIdFromSocket.test.js │ ├── ifMayEdit.test.js │ ├── parseNoteIdFromSocket.test.js │ ├── realtime.test.js │ ├── saveRevisionJob.test.js │ ├── socket-events.test.js │ ├── updateNote.test.js │ └── utils.js └── testDoubles │ ├── ProcessQueueFake.js │ ├── loggerFake.js │ ├── otFake.js │ └── realtimeJobStub.js ├── utils └── string.js ├── webpack.common.js ├── webpack.dev.js ├── webpack.htmlexport.js └── webpack.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": "6", 6 | "uglify": true 7 | } 8 | }] 9 | ], 10 | "plugins": [ 11 | "transform-runtime" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.buildpacks: -------------------------------------------------------------------------------- 1 | https://github.com/alex88/heroku-buildpack-vips 2 | https://github.com/Scalingo/nodejs-buildpack 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | coverage 3 | node_modules/ 4 | 5 | # ignore config files 6 | config.json 7 | .sequelizerc 8 | 9 | # ignore webpack build 10 | public/build 11 | public/views/build 12 | 13 | .nyc_output 14 | coverage/ 15 | 16 | #ignore docker-helper when building 17 | docker-helper 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [{*.html,*.ejs}] 10 | indent_style = space 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [{.travis.yml,npm-shrinkwrap.json,package.json}] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | composer.phar 3 | composer.lock 4 | .env.*.php 5 | .env.php 6 | .DS_Store 7 | .idea/ 8 | Thumbs.db 9 | npm-debug.log 10 | hackmd_io 11 | newrelic_agent.log 12 | logs/ 13 | tmp/ 14 | backups/ 15 | *.pid 16 | *.log 17 | *.sqlite 18 | 19 | # ignore config files 20 | config.json 21 | .sequelizerc 22 | 23 | # ignore webpack build 24 | public/build 25 | public/views/build 26 | 27 | public/uploads/* 28 | !public/uploads/.gitkeep 29 | /.nyc_output 30 | /coverage/ 31 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Max Wu Wu Cheng-Han 2 | Max Wu Cheng-Han, Wu 3 | Max Wu jackycute 4 | Max Wu Wu, Cheng-Han 5 | Max Wu jackycute 6 | 7 | Sheogorath Christoph (Sheogorath) Kern 8 | 9 | Raccoon Raccoon Li 10 | Raccoon Raccoon 11 | 12 | Peter Dave Hello Peter Dave Hello 13 | 14 | Claudius Coenen Claudius Coenen 15 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.20.1 2 | -------------------------------------------------------------------------------- /.sequelizerc.example: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('./lib/config') 3 | 4 | module.exports = { 5 | config: path.resolve('config.json'), 6 | 'migrations-path': path.resolve('lib', 'migrations'), 7 | 'models-path': path.resolve('lib', 'models'), 8 | url: process.env['CMD_DB_URL'] || config.dbURL 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "lts/dubnium" 5 | - "11" 6 | - "12" 7 | 8 | dist: xenial 9 | cache: npm 10 | 11 | matrix: 12 | fast_finish: true 13 | include: 14 | - node_js: lts/dubnium 15 | allow_failures: 16 | - node_js: "11" 17 | - node_js: "12" 18 | 19 | script: 20 | - npm run test:ci 21 | - npm run build 22 | 23 | jobs: 24 | include: 25 | - stage: doctoc-check 26 | install: npm install -g doctoc 27 | if: type = pull_request OR branch = master 28 | script: 29 | - cp README.md README.md.orig 30 | - npm run doctoc 31 | - diff -q README.md README.md.orig 32 | node_js: lts/carbon 33 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | alecdwm 2 | bananaappletw 3 | Bartlomiej Szala 4 | BoHong Li 5 | Bryan Davis 6 | butlerx 7 | Cheng-Han, Wu 8 | Christian Schuhmann 9 | Colin Maudry 10 | Dmytro Kytsmen 11 | Fabien Meghazi 12 | Florian Rhiem 13 | geekyd 14 | GhiMax 15 | greenkeeperio-bot 16 | Himura Kazuto 17 | Ho33e5 18 | Ian Dees 19 | Ikumi Shimizu <193s@users.noreply.github.com> 20 | ivanorsolic 21 | jackycute 22 | jackycute 23 | Jakub Sygnowski 24 | James Stephenson 25 | Jan Kunzmann 26 | Jannik Lorenz 27 | Jason Croft 28 | Johannes Weißl 29 | Jordan Matelsky 30 | Jun SAKATA 31 | Kaiyu Shi 32 | knjcode 33 | Kotaro Yamamoto 34 | Lars Karlsson 35 | Laura Kyle 36 | LluisArevalo 37 | Marcelo Alencar 38 | Martijnpold 39 | Max Wu 40 | neopostmodern 41 | NV 42 | Ömer Erdinç Yağmurlu 43 | p0v1n0m 44 | Pablo Guerrero 45 | Pablo Guerrero 46 | Paras 47 | Patrick Andersen 48 | Peter Dave Hello 49 | Peter Dave Hello 50 | Philipp Zumstein 51 | Raccoon Li 52 | robert 53 | Sergio Valverde 54 | Sheogorath 55 | Simon Joda Stößer 56 | S.Noda 57 | Stratos Gerakakis 58 | The Gitter Badger 59 | tkqubo 60 | tkykm 61 | Tom Wyckhuys 62 | Wonder Chang 63 | Wu Cheng-Han 64 | Xavier Marques 65 | xnum 66 | Yukai Huang 67 | zachariast 68 | Zankio 69 | 蒼時弦也 70 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./bin/heroku_start.sh 2 | -------------------------------------------------------------------------------- /bin/heroku: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ ! -z "$DYNO" ]; then 6 | # setup config files 7 | cp .sequelizerc.example .sequelizerc 8 | 9 | cat << EOF > config.json 10 | 11 | { 12 | "production": { 13 | } 14 | } 15 | 16 | EOF 17 | 18 | fi 19 | -------------------------------------------------------------------------------- /bin/heroku_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | CMD_DB_URL="$DATABASE_URL" CMD_PORT="$PORT" npm run start 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # run command at repo root 6 | CURRENT_PATH=$PWD 7 | if [ -d .git ]; then 8 | cd "$(git rev-parse --show-toplevel)" 9 | fi 10 | 11 | if ! type npm > /dev/null 12 | then 13 | cat << EOF 14 | npm is not installed, please install Node.js and npm. 15 | Read more on Node.js official website: https://nodejs.org 16 | Setup will not be run 17 | EOF 18 | exit 0 19 | fi 20 | 21 | echo "copy config files" 22 | if [ ! -f config.json ]; then 23 | cp config.json.example config.json 24 | fi 25 | 26 | if [ ! -f .sequelizerc ]; then 27 | cp .sequelizerc.example .sequelizerc 28 | fi 29 | 30 | echo "install packages" 31 | npm install 32 | 33 | cat << EOF 34 | 35 | 36 | Edit the following config file to setup CodiLIA server and client. 37 | Read more info at https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-configuration 38 | 39 | * config.json -- CodiLIA config 40 | * .sequelizerc -- db config 41 | 42 | EOF 43 | 44 | # change directory back 45 | cd "$CURRENT_PATH" 46 | -------------------------------------------------------------------------------- /contribute/developer-certificate-of-origin: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | Developer's Certificate of Origin 1.1 12 | 13 | By making a contribution to this project, I certify that: 14 | 15 | (a) The contribution was created in whole or in part by me and I 16 | have the right to submit it under the open source license 17 | indicated in the file; or 18 | 19 | (b) The contribution is based upon previous work that, to the best 20 | of my knowledge, is covered under an appropriate open source 21 | license and I have the right under that license to submit that 22 | work with modifications, whether created in whole or in part 23 | by me, under the same open source license (unless I am 24 | permitted to submit under a different license), as indicated 25 | in the file; or 26 | 27 | (c) The contribution was provided directly to me by some other 28 | person who certified (a), (b) or (c) and I have not modified 29 | it. 30 | 31 | (d) I understand and agree that this project and the contribution 32 | are public and that a record of the contribution (including all 33 | personal information I submit with it, including my sign-off) is 34 | maintained indefinitely and may be redistributed consistent with 35 | this project or the open source license(s) involved. 36 | -------------------------------------------------------------------------------- /deployments/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM codilia/build as BUILD 2 | 3 | COPY --chown=hackmd:hackmd . . 4 | 5 | RUN set -xe && \ 6 | git reset --hard && \ 7 | git clean -fx && \ 8 | npm install && \ 9 | npm run build && \ 10 | cp ./deployments/docker-entrypoint.sh ./ && \ 11 | cp .sequelizerc.example .sequelizerc && \ 12 | rm -rf .git .gitignore .travis.yml .dockerignore .editorconfig .babelrc .mailmap .sequelizerc.example \ 13 | test docs contribute \ 14 | package-lock.json webpack.prod.js webpack.htmlexport.js webpack.dev.js webpack.common.js \ 15 | config.json.example README.md CONTRIBUTING.md AUTHORS node_modules 16 | 17 | FROM codilia/runtime 18 | USER hackmd 19 | WORKDIR /home/hackmd/app 20 | COPY --chown=1500:1500 --from=BUILD /home/hackmd/app . 21 | RUN npm install --production && npm cache clean --force && rm -rf /tmp/{core-js-banners,phantomjs} 22 | EXPOSE 3000 23 | ENTRYPOINT ["/home/hackmd/app/docker-entrypoint.sh"] 24 | -------------------------------------------------------------------------------- /deployments/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | set -x 5 | 6 | CURRENT_DIR=$(dirname "$BASH_SOURCE") 7 | 8 | docker build -t liascript/codilia -f "$CURRENT_DIR/Dockerfile" "$CURRENT_DIR/.." 9 | -------------------------------------------------------------------------------- /deployments/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | database: 4 | image: postgres:11.6-alpine 5 | environment: 6 | - POSTGRES_USER=codimd 7 | - POSTGRES_PASSWORD=change_password 8 | - POSTGRES_DB=codimd 9 | volumes: 10 | - "database-data:/var/lib/postgresql/data" 11 | restart: always 12 | codimd: 13 | # you can use image or custom build below 14 | #image: nabo.codimd.dev/hackmdio/hackmd:2.0.0 15 | #image: hackmdio/codimd 16 | image: liascript/codilia:latest 17 | # build: 18 | # context: .. 19 | # dockerfile: ./deployments/Dockerfile 20 | environment: 21 | - CMD_DB_URL=postgres://codimd:change_password@database/codimd 22 | - CMD_USECDN=false 23 | - CMD_ALLOW_ANONYMOUS=true 24 | # - CMD_SHARE_URL=https://LiaScript.github.io/course/?https://yourserver,running.codi.md 25 | # - CMD_RESPONSIVEVOICE_KEY=12345678 26 | # - CMD_DOMAIN=localhost:4000 27 | depends_on: 28 | - database 29 | ports: 30 | - "4000:3000" 31 | volumes: 32 | - upload-data:/home/hackmd/app/public/uploads 33 | restart: always 34 | volumes: 35 | database-data: {} 36 | upload-data: {} 37 | -------------------------------------------------------------------------------- /deployments/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if [[ "$#" -gt 0 ]]; then 6 | exec "$@" 7 | exit $? 8 | fi 9 | 10 | # check database and redis is ready 11 | pcheck -env CMD_DB_URL 12 | 13 | # run DB migrate 14 | NEED_MIGRATE=${CMD_AUTO_MIGRATE:=true} 15 | 16 | if [[ "$NEED_MIGRATE" = "true" ]] && [[ -f .sequelizerc ]] ; then 17 | npx sequelize db:migrate 18 | fi 19 | 20 | # start application 21 | node app.js 22 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mariadb: 4 | image: linuxserver/mariadb:latest 5 | container_name: codilia_mariadb 6 | restart: always 7 | volumes: 8 | - path to mariadb data:/config 9 | environment: 10 | - MYSQL_ROOT_PASSWORD=secret password 11 | - MYSQL_DATABASE=codilia 12 | - MYSQL_USER=codilia 13 | - MYSQL_PASSWORD=secret password 14 | - PGID=1000 15 | - PUID=1000 16 | - TZ=Europe/London 17 | codilia: 18 | build: . 19 | container_name: codilia 20 | restart: always 21 | depends_on: 22 | - mariadb 23 | volumes: 24 | - path to config:/config 25 | environment: 26 | - DB_HOST=mariadb 27 | - DB_USER=codilia 28 | - DB_PASS=secret password 29 | - DB_NAME=codilia 30 | - DB_PORT=3306 31 | - PGID=1000 32 | - PUID=1000 33 | - TZ=Europe/London 34 | ports: 35 | - "3000:3000" 36 | -------------------------------------------------------------------------------- /docker-helper/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: build-docker runtime-docker 3 | 4 | build-docker: 5 | docker build -t codilia/build -f "build/Dockerfile" "build" 6 | 7 | runtime-docker: runtime/fonts 8 | docker build -t codilia/runtime -f "runtime/Dockerfile" "runtime" 9 | 10 | runtime/fonts:Noto-unhinted.zip 11 | unzip -d $@ $< 12 | 13 | Noto-unhinted.zip: 14 | wget https://noto-website-2.storage.googleapis.com/pkgs/Noto-unhinted.zip 15 | -------------------------------------------------------------------------------- /docker-helper/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-buster 2 | 3 | ARG USER_NAME=hackmd 4 | ARG UID=1500 5 | ARG GID=1500 6 | 7 | RUN set -xe && \ 8 | apt-get update && \ 9 | # install postgres client 10 | apt-get install -y --no-install-recommends apt-transport-https lsb-release && \ 11 | echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ 12 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ 13 | apt-get update && \ 14 | apt-get install -y --no-install-recommends postgresql-client-9.6 && \ 15 | rm -rf /var/lib/apt/lists/* && \ 16 | # upgrade npm to 6.10 17 | npm i -g npm@6.10.3 && \ 18 | # install node-prune 19 | npm i -g node-prune && npm cache clean --force && \ 20 | # Add user and groupd 21 | groupadd --gid $GID $USER_NAME && \ 22 | useradd --uid $UID --gid $USER_NAME --no-log-init --create-home $USER_NAME && \ 23 | mkdir /home/$USER_NAME/.npm && \ 24 | echo "prefix=/home/$USER_NAME/.npm/" > /home/$USER_NAME/.npmrc && \ 25 | # setup github ssh key 26 | mkdir -p /home/hackmd/.ssh && \ 27 | ssh-keyscan -H github.com >> /home/hackmd/.ssh/known_hosts && \ 28 | # setup git credential helper 29 | mkdir -p /home/hackmd/git && \ 30 | git config --global credential.helper 'store --file /home/hackmd/git/credentials' && \ 31 | # setup app dir 32 | mkdir -p /home/$USER_NAME/app && \ 33 | # adjust permission 34 | chown -R $USER_NAME:$USER_NAME /home/$USER_NAME 35 | 36 | USER hackmd 37 | ENV PATH="/home/hackmd/.npm/bin:$PATH" 38 | WORKDIR /home/$USER_NAME/app 39 | ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] 40 | CMD ["node"] 41 | -------------------------------------------------------------------------------- /docker-helper/runtime/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-buster 2 | 3 | ENV NODE_ENV=production 4 | 5 | ARG USER_NAME=hackmd 6 | ARG UID=1500 7 | ARG GID=1500 8 | 9 | ADD fonts/*.otf /usr/share/fonts/opentype/noto/ 10 | ADD fonts/*.ttf /usr/share/fonts/truetype/noto/ 11 | # add font conf for fonts orders 12 | ADD local.conf /etc/fonts/ 13 | #irgendwie funktioniert das nicht 14 | # ADD portchecker-linux-amd64.tar.gz . 15 | 16 | RUN set -xe && \ 17 | apt-get update && \ 18 | # install postgres client 19 | apt-get install -y --no-install-recommends apt-transport-https lsb-release && \ 20 | echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ 21 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ 22 | apt-get update && \ 23 | apt-get install -y --no-install-recommends postgresql-client-9.6 && \ 24 | rm -rf /var/lib/apt/lists/* && \ 25 | # install pchecker 26 | wget https://github.com/hackmdio/portchecker/releases/download/v1.0.5/portchecker-linux-amd64.tar.gz && \ 27 | tar xvf portchecker-linux-amd64.tar.gz -C /usr/local/bin && \ 28 | mv /usr/local/bin/portchecker-linux-amd64 /usr/local/bin/pcheck && \ 29 | rm portchecker-linux-amd64.tar.gz && \ 30 | # Add user and groupd 31 | groupadd --gid $GID $USER_NAME && \ 32 | useradd --uid $UID --gid $USER_NAME --no-log-init --create-home $USER_NAME && \ 33 | mkdir /home/$USER_NAME/.npm && \ 34 | echo "prefix=/home/$USER_NAME/.npm/" > /home/$USER_NAME/.npmrc && \ 35 | # setup app dir 36 | mkdir -p /home/$USER_NAME/app && \ 37 | # adjust permission 38 | chown -R $USER_NAME:$USER_NAME /home/$USER_NAME && \ 39 | su - hackmd -c "npm install -g sequelize-cli@4.1.1 sequelize@4.13.11 npm@6.10.3 pg@6.4.2 && npm cache clean --force" && \ 40 | fc-cache -f -v && \ 41 | dpkg-reconfigure fontconfig-config && \ 42 | dpkg-reconfigure fontconfig 43 | 44 | USER hackmd 45 | ENV PATH="/home/hackmd/.npm/bin:$PATH" 46 | WORKDIR /home/$USER_NAME/app 47 | ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] 48 | CMD ["node"] 49 | 50 | -------------------------------------------------------------------------------- /docker-helper/runtime/local.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | false 6 | hintnone 7 | false 8 | lcddefault 9 | 10 | 11 | 12 | sans-serif 13 | 14 | 15 | Noto Sans 16 | Noto Sans TC 17 | Noto Sans SC 18 | Noto Sans JP 19 | Noto Sans KR 20 | 21 | 22 | regular 23 | 24 | 25 | 26 | 27 | 28 | Noto Sans CJK 29 | 30 | 31 | DemiLight 32 | 33 | 34 | 55 35 | 36 | 37 | 38 | 39 | 40 | Noto Sans CJK 41 | 42 | 43 | Thin 44 | 45 | 46 | 40 47 | 48 | 49 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM lsiobase/ubuntu:bionic 2 | 3 | # set version label 4 | ARG BUILD_DATE 5 | ARG VERSION 6 | ARG CODIMD_RELEASE 7 | LABEL build_version="Linuxserver.io version:- ${VERSION} Build-date:- ${BUILD_DATE}" 8 | LABEL maintainer="chbmb" 9 | 10 | # environment settings 11 | ARG DEBIAN_FRONTEND="noninteractive" 12 | ENV NODE_ENV production 13 | 14 | RUN echo "**** install build packages ****" && \ 15 | apt-get update && \ 16 | apt-get install -y \ 17 | git \ 18 | gnupg \ 19 | jq \ 20 | libssl-dev 21 | 22 | RUN echo "**** install runtime *****" && \ 23 | curl -s https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \ 24 | echo 'deb https://deb.nodesource.com/node_10.x bionic main' > /etc/apt/sources.list.d/nodesource.list 25 | 26 | RUN echo "**** install yarn repository ****" && \ 27 | curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ 28 | echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list && \ 29 | apt-get update && \ 30 | apt-get install -y \ 31 | fontconfig \ 32 | fonts-noto \ 33 | netcat-openbsd \ 34 | nodejs \ 35 | yarn && \ 36 | echo "**** install codi-lia ****" && \ 37 | npm install -g webpack && \ 38 | git clone https://github.com/liascript/codilia /opt/codilia && \ 39 | cd /opt/codilia && \ 40 | rm package-lock.json && \ 41 | npm install \ 42 | npm run build && \ 43 | echo "**** cleanup ****" && \ 44 | yarn cache clean && \ 45 | apt-get -y purge \ 46 | git \ 47 | gnupg \ 48 | jq \ 49 | libssl-dev && \ 50 | apt-get -y autoremove && \ 51 | rm -rf \ 52 | /tmp/* \ 53 | /var/lib/apt/lists/* \ 54 | /var/tmp/* 55 | 56 | # add local files 57 | #COPY root/ / 58 | 59 | # ports and volumes 60 | EXPOSE 3000 61 | VOLUME /config 62 | -------------------------------------------------------------------------------- /lib/auth/bitbucket/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const BitbucketStrategy = require('passport-bitbucket-oauth2').Strategy 6 | const config = require('../../config') 7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 8 | 9 | const bitbucketAuth = module.exports = Router() 10 | 11 | passport.use(new BitbucketStrategy({ 12 | clientID: config.bitbucket.clientID, 13 | clientSecret: config.bitbucket.clientSecret, 14 | callbackURL: config.serverURL + '/auth/bitbucket/callback' 15 | }, passportGeneralCallback)) 16 | 17 | bitbucketAuth.get('/auth/bitbucket', function (req, res, next) { 18 | setReturnToFromReferer(req) 19 | passport.authenticate('bitbucket')(req, res, next) 20 | }) 21 | 22 | // bitbucket auth callback 23 | bitbucketAuth.get('/auth/bitbucket/callback', 24 | passport.authenticate('bitbucket', { 25 | successReturnToOrRedirect: config.serverURL + '/', 26 | failureRedirect: config.serverURL + '/' 27 | }) 28 | ) 29 | -------------------------------------------------------------------------------- /lib/auth/dropbox/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const DropboxStrategy = require('passport-dropbox-oauth2').Strategy 6 | const config = require('../../config') 7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 8 | 9 | const dropboxAuth = module.exports = Router() 10 | 11 | passport.use(new DropboxStrategy({ 12 | apiVersion: '2', 13 | clientID: config.dropbox.clientID, 14 | clientSecret: config.dropbox.clientSecret, 15 | callbackURL: config.serverURL + '/auth/dropbox/callback' 16 | }, passportGeneralCallback)) 17 | 18 | dropboxAuth.get('/auth/dropbox', function (req, res, next) { 19 | setReturnToFromReferer(req) 20 | passport.authenticate('dropbox-oauth2')(req, res, next) 21 | }) 22 | 23 | // dropbox auth callback 24 | dropboxAuth.get('/auth/dropbox/callback', 25 | passport.authenticate('dropbox-oauth2', { 26 | successReturnToOrRedirect: config.serverURL + '/', 27 | failureRedirect: config.serverURL + '/' 28 | }) 29 | ) 30 | -------------------------------------------------------------------------------- /lib/auth/email/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const validator = require('validator') 6 | const LocalStrategy = require('passport-local').Strategy 7 | const config = require('../../config') 8 | const models = require('../../models') 9 | const logger = require('../../logger') 10 | const { setReturnToFromReferer } = require('../utils') 11 | const { urlencodedParser } = require('../../utils') 12 | const response = require('../../response') 13 | 14 | const emailAuth = module.exports = Router() 15 | 16 | passport.use(new LocalStrategy({ 17 | usernameField: 'email' 18 | }, async function (email, password, done) { 19 | if (!validator.isEmail(email)) return done(null, false) 20 | 21 | try { 22 | const user = await models.User.findOne({ 23 | where: { 24 | email: email 25 | } 26 | }) 27 | 28 | if (!user) return done(null, false) 29 | if (!await user.verifyPassword(password)) return done(null, false) 30 | return done(null, user) 31 | } catch (err) { 32 | logger.error(err) 33 | return done(err) 34 | } 35 | })) 36 | 37 | if (config.allowEmailRegister) { 38 | emailAuth.post('/register', urlencodedParser, async function (req, res, next) { 39 | if (!req.body.email || !req.body.password) return response.errorBadRequest(req, res) 40 | if (!validator.isEmail(req.body.email)) return response.errorBadRequest(req, res) 41 | try { 42 | const [user, created] = await models.User.findOrCreate({ 43 | where: { 44 | email: req.body.email 45 | }, 46 | defaults: { 47 | password: req.body.password 48 | } 49 | }) 50 | 51 | if (!user) { 52 | req.flash('error', 'Failed to register your account, please try again.') 53 | return res.redirect(config.serverURL + '/') 54 | } 55 | 56 | if (created) { 57 | logger.debug('user registered: ' + user.id) 58 | req.flash('info', "You've successfully registered, please signin.") 59 | } else { 60 | logger.debug('user found: ' + user.id) 61 | req.flash('error', 'This email has been used, please try another one.') 62 | } 63 | return res.redirect(config.serverURL + '/') 64 | } catch (err) { 65 | logger.error('auth callback failed: ' + err) 66 | return response.errorInternalError(req, res) 67 | } 68 | }) 69 | } 70 | 71 | emailAuth.post('/login', urlencodedParser, function (req, res, next) { 72 | if (!req.body.email || !req.body.password) return response.errorBadRequest(req, res) 73 | if (!validator.isEmail(req.body.email)) return response.errorBadRequest(req, res) 74 | setReturnToFromReferer(req) 75 | passport.authenticate('local', { 76 | successReturnToOrRedirect: config.serverURL + '/', 77 | failureRedirect: config.serverURL + '/', 78 | failureFlash: 'Invalid email or password.' 79 | })(req, res, next) 80 | }) 81 | -------------------------------------------------------------------------------- /lib/auth/facebook/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const FacebookStrategy = require('passport-facebook').Strategy 6 | 7 | const config = require('../../config') 8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 9 | 10 | const facebookAuth = module.exports = Router() 11 | 12 | passport.use(new FacebookStrategy({ 13 | clientID: config.facebook.clientID, 14 | clientSecret: config.facebook.clientSecret, 15 | callbackURL: config.serverURL + '/auth/facebook/callback' 16 | }, passportGeneralCallback)) 17 | 18 | facebookAuth.get('/auth/facebook', function (req, res, next) { 19 | setReturnToFromReferer(req) 20 | passport.authenticate('facebook')(req, res, next) 21 | }) 22 | 23 | // facebook auth callback 24 | facebookAuth.get('/auth/facebook/callback', 25 | passport.authenticate('facebook', { 26 | successReturnToOrRedirect: config.serverURL + '/', 27 | failureRedirect: config.serverURL + '/' 28 | }) 29 | ) 30 | -------------------------------------------------------------------------------- /lib/auth/github/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const GithubStrategy = require('passport-github').Strategy 6 | const config = require('../../config') 7 | const response = require('../../response') 8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 9 | const { URL } = require('url') 10 | 11 | const githubAuth = module.exports = Router() 12 | 13 | function githubUrl (path) { 14 | return config.github.enterpriseURL && new URL(path, config.github.enterpriseURL).toString() 15 | } 16 | 17 | passport.use(new GithubStrategy({ 18 | clientID: config.github.clientID, 19 | clientSecret: config.github.clientSecret, 20 | callbackURL: config.serverURL + '/auth/github/callback', 21 | authorizationURL: githubUrl('login/oauth/authorize'), 22 | tokenURL: githubUrl('login/oauth/access_token'), 23 | userProfileURL: githubUrl('api/v3/user') 24 | }, passportGeneralCallback)) 25 | 26 | githubAuth.get('/auth/github', function (req, res, next) { 27 | setReturnToFromReferer(req) 28 | passport.authenticate('github')(req, res, next) 29 | }) 30 | 31 | // github auth callback 32 | githubAuth.get('/auth/github/callback', 33 | passport.authenticate('github', { 34 | successReturnToOrRedirect: config.serverURL + '/', 35 | failureRedirect: config.serverURL + '/' 36 | }) 37 | ) 38 | 39 | // github callback actions 40 | githubAuth.get('/auth/github/callback/:noteId/:action', response.githubActions) 41 | -------------------------------------------------------------------------------- /lib/auth/gitlab/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const GitlabStrategy = require('passport-gitlab2').Strategy 6 | const config = require('../../config') 7 | const response = require('../../response') 8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 9 | const HttpsProxyAgent = require('https-proxy-agent') 10 | 11 | const gitlabAuth = module.exports = Router() 12 | 13 | const gitlabAuthStrategy = new GitlabStrategy({ 14 | baseURL: config.gitlab.baseURL, 15 | clientID: config.gitlab.clientID, 16 | clientSecret: config.gitlab.clientSecret, 17 | scope: config.gitlab.scope, 18 | callbackURL: config.serverURL + '/auth/gitlab/callback' 19 | }, passportGeneralCallback) 20 | 21 | if (process.env['https_proxy']) { 22 | const httpsProxyAgent = new HttpsProxyAgent(process.env['https_proxy']) 23 | gitlabAuthStrategy._oauth2.setAgent(httpsProxyAgent) 24 | } 25 | 26 | passport.use(gitlabAuthStrategy) 27 | 28 | gitlabAuth.get('/auth/gitlab', function (req, res, next) { 29 | setReturnToFromReferer(req) 30 | passport.authenticate('gitlab')(req, res, next) 31 | }) 32 | 33 | // gitlab auth callback 34 | gitlabAuth.get('/auth/gitlab/callback', 35 | passport.authenticate('gitlab', { 36 | successReturnToOrRedirect: config.serverURL + '/', 37 | failureRedirect: config.serverURL + '/' 38 | }) 39 | ) 40 | 41 | if (!config.gitlab.scope || config.gitlab.scope === 'api') { 42 | // gitlab callback actions 43 | gitlabAuth.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions) 44 | } 45 | -------------------------------------------------------------------------------- /lib/auth/google/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | var GoogleStrategy = require('passport-google-oauth20').Strategy 6 | const config = require('../../config') 7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 8 | 9 | const googleAuth = module.exports = Router() 10 | 11 | passport.use(new GoogleStrategy({ 12 | clientID: config.google.clientID, 13 | clientSecret: config.google.clientSecret, 14 | callbackURL: config.serverURL + '/auth/google/callback', 15 | userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo' 16 | }, passportGeneralCallback)) 17 | 18 | googleAuth.get('/auth/google', function (req, res, next) { 19 | setReturnToFromReferer(req) 20 | passport.authenticate('google', { 21 | scope: ['profile'], 22 | hostedDomain: config.google.hostedDomain 23 | })(req, res, next) 24 | }) 25 | // google auth callback 26 | googleAuth.get('/auth/google/callback', 27 | passport.authenticate('google', { 28 | successReturnToOrRedirect: config.serverURL + '/', 29 | failureRedirect: config.serverURL + '/' 30 | }) 31 | ) 32 | -------------------------------------------------------------------------------- /lib/auth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | 6 | const config = require('../config') 7 | const logger = require('../logger') 8 | const models = require('../models') 9 | 10 | const authRouter = module.exports = Router() 11 | 12 | // serialize and deserialize 13 | passport.serializeUser(function (user, done) { 14 | logger.info('serializeUser: ' + user.id) 15 | return done(null, user.id) 16 | }) 17 | 18 | passport.deserializeUser(function (id, done) { 19 | models.User.findOne({ 20 | where: { 21 | id: id 22 | } 23 | }).then(function (user) { 24 | // Don't die on non-existent user 25 | if (user == null) { 26 | return done(null, false, { message: 'Invalid UserID' }) 27 | } 28 | 29 | logger.info('deserializeUser: ' + user.id) 30 | return done(null, user) 31 | }).catch(function (err) { 32 | logger.error(err) 33 | return done(err, null) 34 | }) 35 | }) 36 | 37 | if (config.isFacebookEnable) authRouter.use(require('./facebook')) 38 | if (config.isTwitterEnable) authRouter.use(require('./twitter')) 39 | if (config.isGitHubEnable) authRouter.use(require('./github')) 40 | if (config.isBitbucketEnable) authRouter.use(require('./bitbucket')) 41 | if (config.isGitLabEnable) authRouter.use(require('./gitlab')) 42 | if (config.isMattermostEnable) authRouter.use(require('./mattermost')) 43 | if (config.isDropboxEnable) authRouter.use(require('./dropbox')) 44 | if (config.isGoogleEnable) authRouter.use(require('./google')) 45 | if (config.isLDAPEnable) authRouter.use(require('./ldap')) 46 | if (config.isSAMLEnable) authRouter.use(require('./saml')) 47 | if (config.isOAuth2Enable) authRouter.use(require('./oauth2')) 48 | if (config.isEmailEnable) authRouter.use(require('./email')) 49 | if (config.isOpenIDEnable) authRouter.use(require('./openid')) 50 | 51 | // logout 52 | authRouter.get('/logout', function (req, res) { 53 | if (config.debug && req.isAuthenticated()) { 54 | logger.debug('user logout: ' + req.user.id) 55 | } 56 | req.logout() 57 | res.redirect(config.serverURL + '/') 58 | }) 59 | -------------------------------------------------------------------------------- /lib/auth/mattermost/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('babel-polyfill') 3 | require('isomorphic-fetch') 4 | const Router = require('express').Router 5 | const passport = require('passport') 6 | const MattermostClient = require('mattermost-redux/client/client4').default 7 | const OAuthStrategy = require('passport-oauth2').Strategy 8 | const config = require('../../config') 9 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 10 | 11 | const mattermostAuth = module.exports = Router() 12 | 13 | const mattermostClient = new MattermostClient() 14 | 15 | const mattermostStrategy = new OAuthStrategy({ 16 | authorizationURL: config.mattermost.baseURL + '/oauth/authorize', 17 | tokenURL: config.mattermost.baseURL + '/oauth/access_token', 18 | clientID: config.mattermost.clientID, 19 | clientSecret: config.mattermost.clientSecret, 20 | callbackURL: config.serverURL + '/auth/mattermost/callback' 21 | }, passportGeneralCallback) 22 | 23 | mattermostStrategy.userProfile = (accessToken, done) => { 24 | mattermostClient.setUrl(config.mattermost.baseURL) 25 | mattermostClient.setToken(accessToken) 26 | mattermostClient.getMe() 27 | .then((data) => done(null, data)) 28 | .catch((err) => done(err)) 29 | } 30 | 31 | passport.use(mattermostStrategy) 32 | 33 | mattermostAuth.get('/auth/mattermost', function (req, res, next) { 34 | setReturnToFromReferer(req) 35 | passport.authenticate('oauth2')(req, res, next) 36 | }) 37 | 38 | // mattermost auth callback 39 | mattermostAuth.get('/auth/mattermost/callback', 40 | passport.authenticate('oauth2', { 41 | successReturnToOrRedirect: config.serverURL + '/', 42 | failureRedirect: config.serverURL + '/' 43 | }) 44 | ) 45 | -------------------------------------------------------------------------------- /lib/auth/oauth2/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | 6 | const config = require('../../config') 7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 8 | const { OAuth2CustomStrategy } = require('./strategy') 9 | 10 | const oauth2Auth = module.exports = Router() 11 | 12 | passport.use(new OAuth2CustomStrategy({ 13 | authorizationURL: config.oauth2.authorizationURL, 14 | tokenURL: config.oauth2.tokenURL, 15 | clientID: config.oauth2.clientID, 16 | clientSecret: config.oauth2.clientSecret, 17 | callbackURL: config.serverURL + '/auth/oauth2/callback', 18 | userProfileURL: config.oauth2.userProfileURL, 19 | state: config.oauth2.state, 20 | scope: config.oauth2.scope 21 | }, passportGeneralCallback)) 22 | 23 | oauth2Auth.get('/auth/oauth2', function (req, res, next) { 24 | setReturnToFromReferer(req) 25 | passport.authenticate('oauth2')(req, res, next) 26 | }) 27 | 28 | // github auth callback 29 | oauth2Auth.get('/auth/oauth2/callback', 30 | passport.authenticate('oauth2', { 31 | successReturnToOrRedirect: config.serverURL + '/', 32 | failureRedirect: config.serverURL + '/' 33 | }) 34 | ) 35 | -------------------------------------------------------------------------------- /lib/auth/oauth2/strategy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Strategy, InternalOAuthError } = require('passport-oauth2') 4 | const config = require('../../config') 5 | 6 | function parseProfile (data) { 7 | const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr) 8 | const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr) 9 | const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr) 10 | const photo = extractProfileAttribute(data, config.oauth2.userProfilePhotoAttr) 11 | 12 | if (!username) { 13 | throw new Error('cannot fetch username: please set correct CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR') 14 | } 15 | 16 | return { 17 | id: username, 18 | username: username, 19 | displayName: displayName, 20 | email: email, 21 | photo: photo 22 | } 23 | } 24 | 25 | function extractProfileAttribute (data, path) { 26 | if (!data) return undefined 27 | if (typeof path !== 'string') return undefined 28 | // can handle stuff like `attrs[0].name` 29 | path = path.split('.') 30 | for (const segment of path) { 31 | const m = segment.match(/([\d\w]+)\[(.*)\]/) 32 | if (!m) { 33 | data = data[segment] 34 | } else { 35 | if (m.length < 3) return undefined 36 | if (!data[m[1]]) return undefined 37 | data = data[m[1]][m[2]] 38 | } 39 | if (!data) return undefined 40 | } 41 | return data 42 | } 43 | 44 | class OAuth2CustomStrategy extends Strategy { 45 | constructor (options, verify) { 46 | options.customHeaders = options.customHeaders || {} 47 | super(options, verify) 48 | this.name = 'oauth2' 49 | this._userProfileURL = options.userProfileURL 50 | this._oauth2.useAuthorizationHeaderforGET(true) 51 | } 52 | 53 | userProfile (accessToken, done) { 54 | this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) { 55 | if (err) { 56 | return done(new InternalOAuthError('Failed to fetch user profile', err)) 57 | } 58 | 59 | let profile, json 60 | try { 61 | json = JSON.parse(body) 62 | profile = parseProfile(json) 63 | } catch (ex) { 64 | return done(new InternalOAuthError('Failed to parse user profile' + ex.toString())) 65 | } 66 | 67 | profile.provider = 'oauth2' 68 | 69 | done(null, profile) 70 | }) 71 | } 72 | } 73 | 74 | exports.OAuth2CustomStrategy = OAuth2CustomStrategy 75 | exports.parseProfile = parseProfile 76 | exports.extractProfileAttribute = extractProfileAttribute 77 | -------------------------------------------------------------------------------- /lib/auth/openid/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const OpenIDStrategy = require('@passport-next/passport-openid').Strategy 6 | const config = require('../../config') 7 | const models = require('../../models') 8 | const logger = require('../../logger') 9 | const { urlencodedParser } = require('../../utils') 10 | const { setReturnToFromReferer } = require('../utils') 11 | 12 | const openIDAuth = module.exports = Router() 13 | 14 | passport.use(new OpenIDStrategy({ 15 | returnURL: config.serverURL + '/auth/openid/callback', 16 | realm: config.serverURL, 17 | profile: true 18 | }, function (openid, profile, done) { 19 | var stringifiedProfile = JSON.stringify(profile) 20 | models.User.findOrCreate({ 21 | where: { 22 | profileid: openid 23 | }, 24 | defaults: { 25 | profile: stringifiedProfile 26 | } 27 | }).spread(function (user, created) { 28 | if (user) { 29 | var needSave = false 30 | if (user.profile !== stringifiedProfile) { 31 | user.profile = stringifiedProfile 32 | needSave = true 33 | } 34 | if (needSave) { 35 | user.save().then(function () { 36 | if (config.debug) { logger.info('user login: ' + user.id) } 37 | return done(null, user) 38 | }) 39 | } else { 40 | if (config.debug) { logger.info('user login: ' + user.id) } 41 | return done(null, user) 42 | } 43 | } 44 | }).catch(function (err) { 45 | logger.error('auth callback failed: ' + err) 46 | return done(err, null) 47 | }) 48 | })) 49 | 50 | openIDAuth.post('/auth/openid', urlencodedParser, function (req, res, next) { 51 | setReturnToFromReferer(req) 52 | passport.authenticate('openid')(req, res, next) 53 | }) 54 | 55 | // openID auth callback 56 | openIDAuth.get('/auth/openid/callback', 57 | passport.authenticate('openid', { 58 | successReturnToOrRedirect: config.serverURL + '/', 59 | failureRedirect: config.serverURL + '/' 60 | }) 61 | ) 62 | -------------------------------------------------------------------------------- /lib/auth/twitter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('express').Router 4 | const passport = require('passport') 5 | const TwitterStrategy = require('passport-twitter').Strategy 6 | 7 | const config = require('../../config') 8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils') 9 | 10 | const twitterAuth = module.exports = Router() 11 | 12 | passport.use(new TwitterStrategy({ 13 | consumerKey: config.twitter.consumerKey, 14 | consumerSecret: config.twitter.consumerSecret, 15 | callbackURL: config.serverURL + '/auth/twitter/callback' 16 | }, passportGeneralCallback)) 17 | 18 | twitterAuth.get('/auth/twitter', function (req, res, next) { 19 | setReturnToFromReferer(req) 20 | passport.authenticate('twitter')(req, res, next) 21 | }) 22 | 23 | // twitter auth callback 24 | twitterAuth.get('/auth/twitter/callback', 25 | passport.authenticate('twitter', { 26 | successReturnToOrRedirect: config.serverURL + '/', 27 | failureRedirect: config.serverURL + '/' 28 | }) 29 | ) 30 | -------------------------------------------------------------------------------- /lib/auth/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const models = require('../models') 4 | const config = require('../config') 5 | const logger = require('../logger') 6 | 7 | exports.setReturnToFromReferer = function setReturnToFromReferer (req) { 8 | var referer = req.get('referer') 9 | if (!req.session) req.session = {} 10 | req.session.returnTo = referer 11 | } 12 | 13 | exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) { 14 | var stringifiedProfile = JSON.stringify(profile) 15 | models.User.findOrCreate({ 16 | where: { 17 | profileid: profile.id.toString() 18 | }, 19 | defaults: { 20 | profile: stringifiedProfile, 21 | accessToken: accessToken, 22 | refreshToken: refreshToken 23 | } 24 | }).spread(function (user, created) { 25 | if (user) { 26 | var needSave = false 27 | if (user.profile !== stringifiedProfile) { 28 | user.profile = stringifiedProfile 29 | needSave = true 30 | } 31 | if (user.accessToken !== accessToken) { 32 | user.accessToken = accessToken 33 | needSave = true 34 | } 35 | if (user.refreshToken !== refreshToken) { 36 | user.refreshToken = refreshToken 37 | needSave = true 38 | } 39 | if (needSave) { 40 | user.save().then(function () { 41 | if (config.debug) { logger.info('user login: ' + user.id) } 42 | return done(null, user) 43 | }) 44 | } else { 45 | if (config.debug) { logger.info('user login: ' + user.id) } 46 | return done(null, user) 47 | } 48 | } 49 | }).catch(function (err) { 50 | logger.error('auth callback failed: ' + err) 51 | return done(err, null) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /lib/config/defaultSSL.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | 5 | function getFile (path) { 6 | if (fs.existsSync(path)) { 7 | return path 8 | } 9 | return undefined 10 | } 11 | 12 | module.exports = { 13 | sslKeyPath: getFile('/run/secrets/key.pem'), 14 | sslCertPath: getFile('/run/secrets/cert.pem'), 15 | sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [], 16 | dhParamPath: getFile('/run/secrets/dhparam.pem') 17 | } 18 | -------------------------------------------------------------------------------- /lib/config/dockerSecret.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const basePath = path.resolve('/var/run/secrets/') 7 | 8 | function getSecret (secret) { 9 | const filePath = path.join(basePath, secret) 10 | if (fs.existsSync(filePath)) return fs.readFileSync(filePath) 11 | return undefined 12 | } 13 | 14 | if (fs.existsSync(basePath)) { 15 | module.exports = { 16 | dbURL: getSecret('dburl'), 17 | // ssl path 18 | sslKeyPath: getSecret('sslkeypath'), 19 | sslCertPath: getSecret('sslcertpath'), 20 | sslCAPath: getSecret('sslcapath'), 21 | dhParamPath: getSecret('dhparampath'), 22 | // session 23 | sessionSecret: getSecret('sessionsecret'), 24 | imgur: { 25 | clientID: getSecret('imgur_clientid') 26 | }, 27 | s3: { 28 | accessKeyId: getSecret('s3_acccessKeyId'), 29 | secretAccessKey: getSecret('s3_secretAccessKey') 30 | }, 31 | minio: { 32 | accessKey: getSecret('minio_accessKey'), 33 | secretKey: getSecret('minio_secretKey') 34 | }, 35 | azure: { 36 | connectionString: getSecret('azure_connectionString') 37 | }, 38 | oauth2: { 39 | clientID: getSecret('oauth2_clientID'), 40 | clientSecret: getSecret('oauth2_clientSecret') 41 | }, 42 | facebook: { 43 | clientID: getSecret('facebook_clientID'), 44 | clientSecret: getSecret('facebook_clientSecret') 45 | }, 46 | twitter: { 47 | consumerKey: getSecret('twitter_consumerKey'), 48 | consumerSecret: getSecret('twitter_consumerSecret') 49 | }, 50 | github: { 51 | clientID: getSecret('github_clientID'), 52 | clientSecret: getSecret('github_clientSecret') 53 | }, 54 | gitlab: { 55 | clientID: getSecret('gitlab_clientID'), 56 | clientSecret: getSecret('gitlab_clientSecret') 57 | }, 58 | mattermost: { 59 | clientID: getSecret('mattermost_clientID'), 60 | clientSecret: getSecret('mattermost_clientSecret') 61 | }, 62 | dropbox: { 63 | clientID: getSecret('dropbox_clientID'), 64 | clientSecret: getSecret('dropbox_clientSecret'), 65 | appKey: getSecret('dropbox_appKey') 66 | }, 67 | google: { 68 | clientID: getSecret('google_clientID'), 69 | clientSecret: getSecret('google_clientSecret') 70 | }, 71 | ldap: { 72 | bindCredentials: getSecret('ldap_bindCredentials'), 73 | tlsca: getSecret('ldap_tlsca') 74 | }, 75 | saml: { 76 | idpCert: getSecret('saml_idpCert') 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/config/enum.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.Environment = { 4 | development: 'development', 5 | production: 'production', 6 | test: 'test' 7 | } 8 | 9 | exports.Permission = { 10 | freely: 'freely', 11 | editable: 'editable', 12 | limited: 'limited', 13 | locked: 'locked', 14 | protected: 'protected', 15 | private: 'private' 16 | } 17 | -------------------------------------------------------------------------------- /lib/config/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | exports.toBooleanConfig = function toBooleanConfig (configValue) { 7 | if (configValue && typeof configValue === 'string') { 8 | return (configValue === 'true') 9 | } 10 | return configValue 11 | } 12 | 13 | exports.toArrayConfig = function toArrayConfig (configValue, separator = ',', fallback) { 14 | if (configValue && typeof configValue === 'string') { 15 | return (configValue.split(separator).map(arrayItem => arrayItem.trim())) 16 | } 17 | return fallback 18 | } 19 | 20 | exports.toIntegerConfig = function toIntegerConfig (configValue) { 21 | if (configValue && typeof configValue === 'string') { 22 | return parseInt(configValue) 23 | } 24 | return configValue 25 | } 26 | 27 | exports.getGitCommit = function getGitCommit (repodir) { 28 | if (!fs.existsSync(repodir + '/.git/HEAD')) { 29 | return undefined 30 | } 31 | let reference = fs.readFileSync(repodir + '/.git/HEAD', 'utf8') 32 | if (reference.startsWith('ref: ')) { 33 | reference = reference.substr(5).replace('\n', '') 34 | reference = fs.readFileSync(path.resolve(repodir + '/.git', reference), 'utf8') 35 | } 36 | reference = reference.replace('\n', '') 37 | return reference 38 | } 39 | 40 | exports.getGitHubURL = function getGitHubURL (repo, reference) { 41 | // if it's not a github reference, we handle handle that anyway 42 | if (!repo.startsWith('https://github.com') && !repo.startsWith('git@github.com')) { 43 | return repo 44 | } 45 | if (repo.startsWith('git@github.com') || repo.startsWith('ssh://git@github.com')) { 46 | repo = repo.replace(/^(ssh:\/\/)?git@github.com:/, 'https://github.com/') 47 | } 48 | 49 | if (repo.endsWith('.git')) { 50 | repo = repo.replace(/\.git$/, '/') 51 | } else if (!repo.endsWith('/')) { 52 | repo = repo + '/' 53 | } 54 | return repo + 'tree/' + reference 55 | } 56 | -------------------------------------------------------------------------------- /lib/errorPage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | const { responseError } = require('../response') 5 | 6 | exports.errorForbidden = (req, res) => { 7 | if (req.user) { 8 | return responseError(res, '403', 'Forbidden', 'oh no.') 9 | } 10 | 11 | req.flash('error', 'You are not allowed to access this page. Maybe try logging in?') 12 | res.redirect(config.serverURL + '/') 13 | } 14 | 15 | exports.errorNotFound = (req, res) => { 16 | responseError(res, '404', 'Not Found', 'oops.') 17 | } 18 | 19 | exports.errorInternalError = (req, res) => { 20 | responseError(res, '500', 'Internal Error', 'wtf.') 21 | } 22 | -------------------------------------------------------------------------------- /lib/homepage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const config = require('../config') 6 | const { User } = require('../models') 7 | const logger = require('../logger') 8 | 9 | exports.showIndex = async (req, res) => { 10 | const isLogin = req.isAuthenticated() 11 | const deleteToken = '' 12 | 13 | const data = { 14 | signin: isLogin, 15 | infoMessage: req.flash('info'), 16 | errorMessage: req.flash('error'), 17 | privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')), 18 | termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')), 19 | deleteToken: deleteToken 20 | } 21 | 22 | if (!isLogin) { 23 | return res.render('index.ejs', data) 24 | } 25 | 26 | const user = await User.findOne({ 27 | where: { 28 | id: req.user.id 29 | } 30 | }) 31 | if (user) { 32 | data.deleteToken = user.deleteToken 33 | return res.render('index.ejs', data) 34 | } 35 | 36 | logger.error(`error: user not found with id ${req.user.id}`) 37 | return res.render('index.ejs', data) 38 | } 39 | -------------------------------------------------------------------------------- /lib/imageRouter/azure.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | 4 | const config = require('../config') 5 | const logger = require('../logger') 6 | 7 | const azure = require('azure-storage') 8 | 9 | exports.uploadImage = function (imagePath, callback) { 10 | if (!imagePath || typeof imagePath !== 'string') { 11 | callback(new Error('Image path is missing or wrong'), null) 12 | return 13 | } 14 | 15 | if (!callback || typeof callback !== 'function') { 16 | logger.error('Callback has to be a function') 17 | return 18 | } 19 | 20 | var azureBlobService = azure.createBlobService(config.azure.connectionString) 21 | 22 | azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, result, response) { 23 | if (err) { 24 | callback(new Error(err.message), null) 25 | } else { 26 | azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, response) { 27 | if (err) { 28 | callback(new Error(err.message), null) 29 | } else { 30 | callback(null, azureBlobService.getUrl(config.azure.container, result.name)) 31 | } 32 | }) 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /lib/imageRouter/filesystem.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const fs = require('fs') 5 | const URL = require('url').URL 6 | const path = require('path') 7 | 8 | const config = require('../config') 9 | const logger = require('../logger') 10 | 11 | /** 12 | * generate a random filename for uploaded image 13 | */ 14 | function randomFilename () { 15 | const buf = crypto.randomBytes(16) 16 | return `upload_${buf.toString('hex')}` 17 | } 18 | 19 | /** 20 | * pick a filename not exist in filesystem 21 | * maximum attempt 5 times 22 | */ 23 | function pickFilename (defaultFilename) { 24 | let retryCounter = 5 25 | let filename = defaultFilename 26 | const extname = path.extname(defaultFilename) 27 | while (retryCounter-- > 0) { 28 | if (fs.existsSync(path.join(config.uploadsPath, filename))) { 29 | filename = `${randomFilename()}${extname}` 30 | continue 31 | } 32 | return filename 33 | } 34 | throw new Error('file exists.') 35 | } 36 | 37 | exports.uploadImage = function (imagePath, callback) { 38 | if (!imagePath || typeof imagePath !== 'string') { 39 | callback(new Error('Image path is missing or wrong'), null) 40 | return 41 | } 42 | 43 | if (!callback || typeof callback !== 'function') { 44 | logger.error('Callback has to be a function') 45 | return 46 | } 47 | 48 | let filename = path.basename(imagePath) 49 | try { 50 | filename = pickFilename(path.basename(imagePath)) 51 | } catch (e) { 52 | return callback(e, null) 53 | } 54 | 55 | try { 56 | fs.copyFileSync(imagePath, path.join(config.uploadsPath, filename)) 57 | } catch (e) { 58 | return callback(e, null) 59 | } 60 | 61 | let url 62 | try { 63 | url = (new URL(filename, config.serverURL + '/uploads/')).href 64 | } catch (e) { 65 | url = config.serverURL + '/uploads/' + filename 66 | } 67 | 68 | callback(null, url) 69 | } 70 | -------------------------------------------------------------------------------- /lib/imageRouter/imgur.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const config = require('../config') 3 | const logger = require('../logger') 4 | 5 | const imgur = require('@hackmd/imgur') 6 | 7 | exports.uploadImage = function (imagePath, callback) { 8 | if (!imagePath || typeof imagePath !== 'string') { 9 | callback(new Error('Image path is missing or wrong'), null) 10 | return 11 | } 12 | 13 | if (!callback || typeof callback !== 'function') { 14 | logger.error('Callback has to be a function') 15 | return 16 | } 17 | 18 | imgur.setClientId(config.imgur.clientID) 19 | imgur.uploadFile(imagePath) 20 | .then(function (json) { 21 | if (config.debug) { 22 | logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) 23 | } 24 | callback(null, json.data.link.replace(/^http:\/\//i, 'https://')) 25 | }).catch(function (err) { 26 | callback(new Error(err), null) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /lib/imageRouter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const Router = require('express').Router 5 | const formidable = require('formidable') 6 | 7 | const config = require('../config') 8 | const logger = require('../logger') 9 | const response = require('../response') 10 | 11 | const imageRouter = module.exports = Router() 12 | 13 | // upload image 14 | imageRouter.post('/uploadimage', function (req, res) { 15 | var form = new formidable.IncomingForm() 16 | 17 | form.keepExtensions = true 18 | 19 | form.parse(req, function (err, fields, files) { 20 | if (err || !files.image || !files.image.path) { 21 | response.errorForbidden(req, res) 22 | } else { 23 | if (config.debug) { 24 | logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)) 25 | } 26 | 27 | const uploadProvider = require('./' + config.imageUploadType) 28 | uploadProvider.uploadImage(files.image.path, function (err, url) { 29 | // remove temporary upload file, and ignore any error 30 | fs.unlink(files.image.path, () => {}) 31 | if (err !== null) { 32 | logger.error(err) 33 | return res.status(500).end('upload image error') 34 | } 35 | res.send({ 36 | link: url 37 | }) 38 | }) 39 | } 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /lib/imageRouter/lutim.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const config = require('../config') 3 | const logger = require('../logger') 4 | 5 | const lutim = require('lutim') 6 | 7 | exports.uploadImage = function (imagePath, callback) { 8 | if (!imagePath || typeof imagePath !== 'string') { 9 | callback(new Error('Image path is missing or wrong'), null) 10 | return 11 | } 12 | 13 | if (!callback || typeof callback !== 'function') { 14 | logger.error('Callback has to be a function') 15 | return 16 | } 17 | 18 | if (config.lutim && config.lutim.url) { 19 | lutim.setAPIUrl(config.lutim.url) 20 | } 21 | 22 | lutim.uploadImage(imagePath) 23 | .then(function (json) { 24 | if (config.debug) { 25 | logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) 26 | } 27 | callback(null, lutim.getAPIUrl() + json.msg.short) 28 | }).catch(function (err) { 29 | callback(new Error(err), null) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/imageRouter/minio.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const config = require('../config') 6 | const { getImageMimeType } = require('../utils') 7 | const logger = require('../logger') 8 | 9 | const Minio = require('minio') 10 | const minioClient = new Minio.Client({ 11 | endPoint: config.minio.endPoint, 12 | port: config.minio.port, 13 | useSSL: config.minio.secure, 14 | accessKey: config.minio.accessKey, 15 | secretKey: config.minio.secretKey 16 | }) 17 | 18 | exports.uploadImage = function (imagePath, callback) { 19 | if (!imagePath || typeof imagePath !== 'string') { 20 | callback(new Error('Image path is missing or wrong'), null) 21 | return 22 | } 23 | 24 | if (!callback || typeof callback !== 'function') { 25 | logger.error('Callback has to be a function') 26 | return 27 | } 28 | 29 | fs.readFile(imagePath, function (err, buffer) { 30 | if (err) { 31 | callback(new Error(err), null) 32 | return 33 | } 34 | 35 | const key = path.join('uploads', path.basename(imagePath)) 36 | const protocol = config.minio.secure ? 'https' : 'http' 37 | 38 | minioClient.putObject(config.s3bucket, key, buffer, buffer.size, getImageMimeType(imagePath), function (err, data) { 39 | if (err) { 40 | callback(new Error(err), null) 41 | return 42 | } 43 | const hidePort = [80, 443].includes(config.minio.port) 44 | const urlPort = hidePort ? '' : `:${config.minio.port}` 45 | callback(null, `${protocol}://${config.minio.endPoint}${urlPort}/${config.s3bucket}/${key}`) 46 | }) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /lib/imageRouter/s3.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | const config = require('../config') 6 | const { getImageMimeType } = require('../utils') 7 | const logger = require('../logger') 8 | 9 | const { S3Client } = require('@aws-sdk/client-s3-node/S3Client') 10 | const { PutObjectCommand } = require('@aws-sdk/client-s3-node/commands/PutObjectCommand') 11 | 12 | const s3 = new S3Client(config.s3) 13 | 14 | exports.uploadImage = function (imagePath, callback) { 15 | if (!imagePath || typeof imagePath !== 'string') { 16 | callback(new Error('Image path is missing or wrong'), null) 17 | return 18 | } 19 | 20 | if (!callback || typeof callback !== 'function') { 21 | logger.error('Callback has to be a function') 22 | return 23 | } 24 | 25 | fs.readFile(imagePath, function (err, buffer) { 26 | if (err) { 27 | callback(new Error(err), null) 28 | return 29 | } 30 | const params = { 31 | Bucket: config.s3bucket, 32 | Key: path.join('uploads', path.basename(imagePath)), 33 | Body: buffer, 34 | ACL: 'public-read' 35 | } 36 | const mimeType = getImageMimeType(imagePath) 37 | if (mimeType) { params.ContentType = mimeType } 38 | 39 | const command = new PutObjectCommand(params) 40 | 41 | s3.send(command).then(data => { 42 | let s3Endpoint = 's3.amazonaws.com' 43 | if (config.s3.endpoint) { 44 | s3Endpoint = config.s3.endpoint 45 | } else if (config.s3.region && config.s3.region !== 'us-east-1') { 46 | s3Endpoint = `s3-${config.s3.region}.amazonaws.com` 47 | } 48 | callback(null, `https://${s3Endpoint}/${config.s3bucket}/${params.Key}`) 49 | }).catch(err => { 50 | if (err) { 51 | callback(new Error(err), null) 52 | } 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /lib/letter-avatars.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // external modules 3 | const crypto = require('crypto') 4 | const randomcolor = require('randomcolor') 5 | const config = require('./config') 6 | 7 | // core 8 | exports.generateAvatar = function (name) { 9 | const color = randomcolor({ 10 | seed: name, 11 | luminosity: 'dark' 12 | }) 13 | const letter = name.substring(0, 1).toUpperCase() 14 | 15 | let svg = '' 16 | svg += '' 17 | svg += '' 18 | svg += '' 19 | svg += '' 20 | svg += '' + letter + '' 21 | svg += '' 22 | svg += '' 23 | svg += '' 24 | 25 | return svg 26 | } 27 | 28 | exports.generateAvatarURL = function (name, email = '', big = true) { 29 | let photo 30 | if (typeof email !== 'string') { 31 | email = '' + name + '@example.com' 32 | } 33 | name = encodeURIComponent(name) 34 | 35 | const hash = crypto.createHash('md5') 36 | hash.update(email.toLowerCase()) 37 | const hexDigest = hash.digest('hex') 38 | 39 | if (email !== '' && config.allowGravatar) { 40 | photo = 'https://www.gravatar.com/avatar/' + hexDigest 41 | if (big) { 42 | photo += '?s=400' 43 | } else { 44 | photo += '?s=96' 45 | } 46 | } else { 47 | photo = config.serverURL + '/user/' + (name || email.substring(0, email.lastIndexOf('@')) || hexDigest) + '/avatar.svg' 48 | } 49 | return photo 50 | } 51 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { createLogger, format, transports } = require('winston') 3 | 4 | const logger = createLogger({ 5 | level: 'debug', 6 | format: format.combine( 7 | format.uncolorize(), 8 | format.timestamp(), 9 | format.align(), 10 | format.splat(), 11 | format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`) 12 | ), 13 | transports: [ 14 | new transports.Console({ 15 | handleExceptions: true 16 | }) 17 | ], 18 | exitOnError: false 19 | }) 20 | 21 | logger.stream = { 22 | write: function (message, encoding) { 23 | logger.info(message) 24 | } 25 | } 26 | 27 | module.exports = logger 28 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Router } = require('express') 4 | 5 | const { wrap } = require('./utils') 6 | 7 | // load controller 8 | const statusController = require('./status') 9 | const appRouter = Router() 10 | 11 | // register route 12 | appRouter.get('/status', wrap(statusController.getStatus)) 13 | appRouter.get('/metrics/codimd', wrap(statusController.getMetrics)) 14 | 15 | exports.router = appRouter 16 | -------------------------------------------------------------------------------- /lib/middleware/checkURIValid.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const logger = require('../logger') 4 | const response = require('../response') 5 | 6 | module.exports = function (req, res, next) { 7 | try { 8 | decodeURIComponent(req.path) 9 | } catch (err) { 10 | logger.error(err) 11 | return response.errorBadRequest(req, res) 12 | } 13 | next() 14 | } 15 | -------------------------------------------------------------------------------- /lib/middleware/codiMDVersion.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | 5 | module.exports = function (req, res, next) { 6 | res.set({ 7 | 'CodiMD-Version': config.version 8 | }) 9 | return next() 10 | } 11 | -------------------------------------------------------------------------------- /lib/middleware/redirectWithoutTrailingSlashes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | 5 | module.exports = function (req, res, next) { 6 | if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) { 7 | const queryString = req.url.slice(req.path.length) 8 | const urlPath = req.path.slice(0, -1) 9 | let serverURL = config.serverURL 10 | if (config.urlPath) { 11 | serverURL = serverURL.slice(0, -(config.urlPath.length + 1)) 12 | } 13 | res.redirect(301, serverURL + urlPath + queryString) 14 | } else { 15 | next() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/middleware/tooBusy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const toobusy = require('toobusy-js') 4 | 5 | const config = require('../config') 6 | const response = require('../response') 7 | 8 | toobusy.maxLag(config.responseMaxLag) 9 | 10 | module.exports = function (req, res, next) { 11 | if (toobusy()) { 12 | response.errorServiceUnavailable(req, res) 13 | } else { 14 | next() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/migrations/20150504155329-create-users.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.createTable('Users', { 5 | id: { 6 | type: Sequelize.UUID, 7 | primaryKey: true, 8 | defaultValue: Sequelize.UUIDV4 9 | }, 10 | profileid: { 11 | type: Sequelize.STRING, 12 | unique: true 13 | }, 14 | profile: Sequelize.TEXT, 15 | history: Sequelize.TEXT, 16 | createdAt: Sequelize.DATE, 17 | updatedAt: Sequelize.DATE 18 | }) 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | return queryInterface.dropTable('Users') 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/migrations/20150508114741-create-notes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.createTable('Notes', { 5 | id: { 6 | type: Sequelize.UUID, 7 | primaryKey: true, 8 | defaultValue: Sequelize.UUIDV4 9 | }, 10 | ownerId: Sequelize.UUID, 11 | content: Sequelize.TEXT, 12 | title: Sequelize.STRING, 13 | createdAt: Sequelize.DATE, 14 | updatedAt: Sequelize.DATE 15 | }) 16 | }, 17 | 18 | down: function (queryInterface, Sequelize) { 19 | return queryInterface.dropTable('Notes') 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/migrations/20150515125813-create-temp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.createTable('Temp', { 5 | id: { 6 | type: Sequelize.STRING, 7 | primaryKey: true 8 | }, 9 | date: Sequelize.TEXT, 10 | createdAt: Sequelize.DATE, 11 | updatedAt: Sequelize.DATE 12 | }) 13 | }, 14 | 15 | down: function (queryInterface, Sequelize) { 16 | return queryInterface.dropTable('Temp') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/migrations/20150702001020-update-to-0_3_1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'shortid', { 5 | type: Sequelize.STRING, 6 | defaultValue: '0000000000', 7 | allowNull: false 8 | }).then(function () { 9 | return queryInterface.addIndex('Notes', ['shortid'], { 10 | indicesType: 'UNIQUE' 11 | }) 12 | }).then(function () { 13 | return queryInterface.addColumn('Notes', 'permission', { 14 | type: Sequelize.STRING, 15 | defaultValue: 'private', 16 | allowNull: false 17 | }) 18 | }).then(function () { 19 | return queryInterface.addColumn('Notes', 'viewcount', { 20 | type: Sequelize.INTEGER, 21 | defaultValue: 0 22 | }) 23 | }).catch(function (error) { 24 | if (error.message === 'SQLITE_ERROR: duplicate column name: shortid' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'shortid'" || error.message === 'column "shortid" of relation "Notes" already exists') { 25 | console.log('Migration has already run… ignoring.') 26 | } else { 27 | throw error 28 | } 29 | }) 30 | }, 31 | 32 | down: function (queryInterface, Sequelize) { 33 | return queryInterface.removeColumn('Notes', 'viewcount') 34 | .then(function () { 35 | return queryInterface.removeColumn('Notes', 'permission') 36 | }) 37 | .then(function () { 38 | return queryInterface.removeIndex('Notes', ['shortid']) 39 | }) 40 | .then(function () { 41 | return queryInterface.removeColumn('Notes', 'shortid') 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/migrations/20150915153700-change-notes-title-to-text.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const isSQLite = require('../utils').isSQLite 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.changeColumn('Notes', 'title', { 6 | type: Sequelize.TEXT 7 | }).then(function () { 8 | if (isSQLite(queryInterface.sequelize)) { 9 | // manual added index will be removed in sqlite 10 | return queryInterface.addIndex('Notes', ['shortid']) 11 | } 12 | }) 13 | }, 14 | 15 | down: function (queryInterface, Sequelize) { 16 | return queryInterface.changeColumn('Notes', 'title', { 17 | type: Sequelize.STRING 18 | }).then(function () { 19 | if (isSQLite(queryInterface.sequelize)) { 20 | // manual added index will be removed in sqlite 21 | return queryInterface.addIndex('Notes', ['shortid']) 22 | } 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/migrations/20160112220142-note-add-lastchange.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'lastchangeuserId', { 5 | type: Sequelize.UUID 6 | }).then(function () { 7 | return queryInterface.addColumn('Notes', 'lastchangeAt', { 8 | type: Sequelize.DATE 9 | }) 10 | }).catch(function (error) { 11 | if (error.message === 'SQLITE_ERROR: duplicate column name: lastchangeuserId' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'lastchangeuserId'" || error.message === 'column "lastchangeuserId" of relation "Notes" already exists') { 12 | console.log('Migration has already run… ignoring.') 13 | } else { 14 | throw error 15 | } 16 | }) 17 | }, 18 | 19 | down: function (queryInterface, Sequelize) { 20 | return queryInterface.removeColumn('Notes', 'lastchangeAt') 21 | .then(function () { 22 | return queryInterface.removeColumn('Notes', 'lastchangeuserId') 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/migrations/20160420180355-note-add-alias.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'alias', { 5 | type: Sequelize.STRING 6 | }).then(function () { 7 | return queryInterface.addIndex('Notes', ['alias'], { 8 | indicesType: 'UNIQUE' 9 | }) 10 | }).catch(function (error) { 11 | if (error.message === 'SQLITE_ERROR: duplicate column name: alias' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'alias'" || error.message === 'column "alias" of relation "Notes" already exists') { 12 | console.log('Migration has already run… ignoring.') 13 | } else { 14 | throw error 15 | } 16 | }) 17 | }, 18 | 19 | down: function (queryInterface, Sequelize) { 20 | return queryInterface.removeColumn('Notes', 'alias').then(function () { 21 | return queryInterface.removeIndex('Notes', ['alias']) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/migrations/20160515114000-user-add-tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING).then(function () { 5 | return queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING) 6 | }).catch(function (error) { 7 | if (error.message === 'SQLITE_ERROR: duplicate column name: accessToken' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'accessToken'" || error.message === 'column "accessToken" of relation "Users" already exists') { 8 | console.log('Migration has already run… ignoring.') 9 | } else { 10 | throw error 11 | } 12 | }) 13 | }, 14 | 15 | down: function (queryInterface, Sequelize) { 16 | return queryInterface.removeColumn('Users', 'accessToken').then(function () { 17 | return queryInterface.removeColumn('Users', 'refreshToken') 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/migrations/20160607060246-support-revision.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE).then(function () { 5 | return queryInterface.createTable('Revisions', { 6 | id: { 7 | type: Sequelize.UUID, 8 | primaryKey: true 9 | }, 10 | noteId: Sequelize.UUID, 11 | patch: Sequelize.TEXT, 12 | lastContent: Sequelize.TEXT, 13 | content: Sequelize.TEXT, 14 | length: Sequelize.INTEGER, 15 | createdAt: Sequelize.DATE, 16 | updatedAt: Sequelize.DATE 17 | }) 18 | }).catch(function (error) { 19 | if (error.message === 'SQLITE_ERROR: duplicate column name: savedAt' | error.message === "ER_DUP_FIELDNAME: Duplicate column name 'savedAt'" || error.message === 'column "savedAt" of relation "Notes" already exists') { 20 | console.log('Migration has already run… ignoring.') 21 | } else { 22 | throw error 23 | } 24 | }) 25 | }, 26 | 27 | down: function (queryInterface, Sequelize) { 28 | return queryInterface.dropTable('Revisions').then(function () { 29 | return queryInterface.removeColumn('Notes', 'savedAt') 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/migrations/20160703062241-support-authorship.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT).then(function () { 5 | return queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT) 6 | }).then(function () { 7 | return queryInterface.createTable('Authors', { 8 | id: { 9 | type: Sequelize.INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | color: Sequelize.STRING, 14 | noteId: Sequelize.UUID, 15 | userId: Sequelize.UUID, 16 | createdAt: Sequelize.DATE, 17 | updatedAt: Sequelize.DATE 18 | }) 19 | }).catch(function (error) { 20 | if (error.message === 'SQLITE_ERROR: duplicate column name: authorship' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'authorship'" || error.message === 'column "authorship" of relation "Notes" already exists') { 21 | console.log('Migration has already run… ignoring.') 22 | } else { 23 | throw error 24 | } 25 | }) 26 | }, 27 | 28 | down: function (queryInterface, Sequelize) { 29 | return queryInterface.dropTable('Authors').then(function () { 30 | return queryInterface.removeColumn('Revisions', 'authorship') 31 | }).then(function () { 32 | return queryInterface.removeColumn('Notes', 'authorship') 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/migrations/20161009040430-support-delete-note.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE).catch(function (error) { 5 | if (error.message === 'SQLITE_ERROR: duplicate column name: deletedAt' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'deletedAt'" || error.message === 'column "deletedAt" of relation "Notes" already exists') { 6 | console.log('Migration has already run… ignoring.') 7 | } else { 8 | throw error 9 | } 10 | }) 11 | }, 12 | 13 | down: function (queryInterface, Sequelize) { 14 | return queryInterface.removeColumn('Notes', 'deletedAt') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/migrations/20161201050312-support-email-signin.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Users', 'email', Sequelize.TEXT).then(function () { 5 | return queryInterface.addColumn('Users', 'password', Sequelize.TEXT).catch(function (error) { 6 | if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'password'" || error.message === 'column "password" of relation "Users" already exists') { 7 | console.log('Migration has already run… ignoring.') 8 | } else { 9 | throw error 10 | } 11 | }) 12 | }).catch(function (error) { 13 | if (error.message === 'SQLITE_ERROR: duplicate column name: email' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'email'" || error.message === 'column "email" of relation "Users" already exists') { 14 | console.log('Migration has already run… ignoring.') 15 | } else { 16 | throw error 17 | } 18 | }) 19 | }, 20 | 21 | down: function (queryInterface, Sequelize) { 22 | return queryInterface.removeColumn('Users', 'email').then(function () { 23 | return queryInterface.removeColumn('Users', 'password') 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/migrations/20171009121200-longtext-for-mysql.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: async function (queryInterface, Sequelize) { 4 | await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') }) 5 | await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') }) 6 | await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') }) 7 | await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') }) 8 | }, 9 | 10 | down: async function (queryInterface, Sequelize) { 11 | await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT }) 12 | await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT }) 13 | await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT }) 14 | await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/migrations/20180209120907-longtext-of-authorship.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: async function (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') }) 6 | await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') }) 7 | }, 8 | 9 | down: async function (queryInterface, Sequelize) { 10 | await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT }) 11 | await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/migrations/20180306150303-fix-enum.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: async function (queryInterface, Sequelize) { 5 | await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') }) 6 | }, 7 | 8 | down: async function (queryInterface, Sequelize) { 9 | await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/migrations/20180326103000-use-text-in-tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: function (queryInterface, Sequelize) { 5 | return queryInterface.changeColumn('Users', 'accessToken', { 6 | type: Sequelize.TEXT 7 | }).then(function () { 8 | return queryInterface.changeColumn('Users', 'refreshToken', { 9 | type: Sequelize.TEXT 10 | }) 11 | }) 12 | }, 13 | 14 | down: function (queryInterface, Sequelize) { 15 | return queryInterface.changeColumn('Users', 'accessToken', { 16 | type: Sequelize.STRING 17 | }).then(function () { 18 | return queryInterface.changeColumn('Users', 'refreshToken', { 19 | type: Sequelize.STRING 20 | }) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/migrations/20180525153000-user-add-delete-token.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | up: function (queryInterface, Sequelize) { 4 | return queryInterface.addColumn('Users', 'deleteToken', { 5 | type: Sequelize.UUID, 6 | defaultValue: Sequelize.UUIDV4 7 | }) 8 | }, 9 | 10 | down: function (queryInterface, Sequelize) { 11 | return queryInterface.removeColumn('Users', 'deleteToken') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/migrations/20200104215332-remove-temp-table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.dropTable('Temp') 6 | /* 7 | Add altering commands here. 8 | Return a promise to correctly handle asynchronicity. 9 | 10 | Example: 11 | return queryInterface.createTable('users', { id: Sequelize.INTEGER }); 12 | */ 13 | }, 14 | 15 | down: (queryInterface, Sequelize) => { 16 | return queryInterface.createTable('Temp', { 17 | id: { 18 | type: Sequelize.STRING, 19 | primaryKey: true 20 | }, 21 | date: Sequelize.TEXT, 22 | createdAt: Sequelize.DATE, 23 | updatedAt: Sequelize.DATE 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/models/author.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // external modules 3 | var Sequelize = require('sequelize') 4 | 5 | module.exports = function (sequelize, DataTypes) { 6 | var Author = sequelize.define('Author', { 7 | id: { 8 | type: Sequelize.INTEGER, 9 | primaryKey: true, 10 | autoIncrement: true 11 | }, 12 | color: { 13 | type: DataTypes.STRING 14 | } 15 | }, { 16 | indexes: [ 17 | { 18 | unique: true, 19 | fields: ['noteId', 'userId'] 20 | } 21 | ] 22 | }) 23 | 24 | Author.associate = function (models) { 25 | Author.belongsTo(models.Note, { 26 | foreignKey: 'noteId', 27 | as: 'note', 28 | constraints: false, 29 | onDelete: 'CASCADE', 30 | hooks: true 31 | }) 32 | Author.belongsTo(models.User, { 33 | foreignKey: 'userId', 34 | as: 'user', 35 | constraints: false, 36 | onDelete: 'CASCADE', 37 | hooks: true 38 | }) 39 | } 40 | 41 | return Author 42 | } 43 | -------------------------------------------------------------------------------- /lib/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // external modules 3 | var fs = require('fs') 4 | var path = require('path') 5 | var Sequelize = require('sequelize') 6 | const { cloneDeep } = require('lodash') 7 | 8 | // core 9 | var config = require('../config') 10 | var logger = require('../logger') 11 | 12 | var dbconfig = cloneDeep(config.db) 13 | dbconfig.logging = config.debug ? (data) => { 14 | logger.info(data) 15 | } : false 16 | 17 | var sequelize = null 18 | 19 | // Heroku specific 20 | if (config.dbURL) { 21 | sequelize = new Sequelize(config.dbURL, dbconfig) 22 | } else { 23 | sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig) 24 | } 25 | 26 | // [Postgres] Handling NULL bytes 27 | // https://github.com/sequelize/sequelize/issues/6485 28 | function stripNullByte (value) { 29 | value = '' + value 30 | // eslint-disable-next-line no-control-regex 31 | return value ? value.replace(/\u0000/g, '') : value 32 | } 33 | sequelize.stripNullByte = stripNullByte 34 | 35 | function processData (data, _default, process) { 36 | if (data === undefined) return data 37 | else return data === null ? _default : (process ? process(data) : data) 38 | } 39 | sequelize.processData = processData 40 | 41 | var db = {} 42 | 43 | fs.readdirSync(__dirname) 44 | .filter(function (file) { 45 | return (file.indexOf('.') !== 0) && (file !== 'index.js') 46 | }) 47 | .forEach(function (file) { 48 | var model = sequelize.import(path.join(__dirname, file)) 49 | db[model.name] = model 50 | }) 51 | 52 | Object.keys(db).forEach(function (modelName) { 53 | if ('associate' in db[modelName]) { 54 | db[modelName].associate(db) 55 | } 56 | }) 57 | 58 | db.sequelize = sequelize 59 | db.Sequelize = Sequelize 60 | 61 | module.exports = db 62 | -------------------------------------------------------------------------------- /lib/ot/index.js: -------------------------------------------------------------------------------- 1 | exports.version = '0.0.15'; 2 | 3 | exports.TextOperation = require('./text-operation'); 4 | exports.SimpleTextOperation = require('./simple-text-operation'); 5 | exports.Client = require('./client'); 6 | exports.Server = require('./server'); 7 | exports.Selection = require('./selection'); 8 | exports.EditorSocketIOServer = require('./editor-socketio-server'); 9 | -------------------------------------------------------------------------------- /lib/ot/server.js: -------------------------------------------------------------------------------- 1 | var config = require('../config'); 2 | 3 | if (typeof ot === 'undefined') { 4 | var ot = {}; 5 | } 6 | 7 | ot.Server = (function (global) { 8 | 'use strict'; 9 | 10 | // Constructor. Takes the current document as a string and optionally the array 11 | // of all operations. 12 | function Server (document, operations) { 13 | this.document = document; 14 | this.operations = operations || []; 15 | } 16 | 17 | // Call this method whenever you receive an operation from a client. 18 | Server.prototype.receiveOperation = function (revision, operation) { 19 | if (revision < 0 || this.operations.length < revision) { 20 | throw new Error("operation revision not in history"); 21 | } 22 | // Find all operations that the client didn't know of when it sent the 23 | // operation ... 24 | var concurrentOperations = this.operations.slice(revision); 25 | 26 | // ... and transform the operation against all these operations ... 27 | var transform = operation.constructor.transform; 28 | for (var i = 0; i < concurrentOperations.length; i++) { 29 | operation = transform(operation, concurrentOperations[i])[0]; 30 | } 31 | 32 | // ... and apply that on the document. 33 | var newDocument = operation.apply(this.document); 34 | // ignore if exceed the max length of document 35 | if(newDocument.length > config.documentMaxLength && newDocument.length > this.document.length) 36 | return; 37 | this.document = newDocument; 38 | // Store operation in history. 39 | this.operations.push(operation); 40 | 41 | // It's the caller's responsibility to send the operation to all connected 42 | // clients and an acknowledgement to the creator. 43 | return operation; 44 | }; 45 | 46 | return Server; 47 | 48 | }(this)); 49 | 50 | if (typeof module === 'object') { 51 | module.exports = ot.Server; 52 | } -------------------------------------------------------------------------------- /lib/ot/wrapped-operation.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.WrappedOperation = (function (global) { 7 | 'use strict'; 8 | 9 | // A WrappedOperation contains an operation and corresponing metadata. 10 | function WrappedOperation (operation, meta) { 11 | this.wrapped = operation; 12 | this.meta = meta; 13 | } 14 | 15 | WrappedOperation.prototype.apply = function () { 16 | return this.wrapped.apply.apply(this.wrapped, arguments); 17 | }; 18 | 19 | WrappedOperation.prototype.invert = function () { 20 | var meta = this.meta; 21 | return new WrappedOperation( 22 | this.wrapped.invert.apply(this.wrapped, arguments), 23 | meta && typeof meta === 'object' && typeof meta.invert === 'function' ? 24 | meta.invert.apply(meta, arguments) : meta 25 | ); 26 | }; 27 | 28 | // Copy all properties from source to target. 29 | function copy (source, target) { 30 | for (var key in source) { 31 | if (source.hasOwnProperty(key)) { 32 | target[key] = source[key]; 33 | } 34 | } 35 | } 36 | 37 | function composeMeta (a, b) { 38 | if (a && typeof a === 'object') { 39 | if (typeof a.compose === 'function') { return a.compose(b); } 40 | var meta = {}; 41 | copy(a, meta); 42 | copy(b, meta); 43 | return meta; 44 | } 45 | return b; 46 | } 47 | 48 | WrappedOperation.prototype.compose = function (other) { 49 | return new WrappedOperation( 50 | this.wrapped.compose(other.wrapped), 51 | composeMeta(this.meta, other.meta) 52 | ); 53 | }; 54 | 55 | function transformMeta (meta, operation) { 56 | if (meta && typeof meta === 'object') { 57 | if (typeof meta.transform === 'function') { 58 | return meta.transform(operation); 59 | } 60 | } 61 | return meta; 62 | } 63 | 64 | WrappedOperation.transform = function (a, b) { 65 | var transform = a.wrapped.constructor.transform; 66 | var pair = transform(a.wrapped, b.wrapped); 67 | return [ 68 | new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)), 69 | new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped)) 70 | ]; 71 | }; 72 | 73 | return WrappedOperation; 74 | 75 | }(this)); 76 | 77 | // Export for CommonJS 78 | if (typeof module === 'object') { 79 | module.exports = ot.WrappedOperation; 80 | } -------------------------------------------------------------------------------- /lib/realtime/processQueue.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | 5 | /** 6 | * Queuing Class for connection queuing 7 | */ 8 | 9 | const QueueEvent = { 10 | Tick: 'Tick', 11 | Push: 'Push', 12 | Finish: 'Finish' 13 | } 14 | 15 | class ProcessQueue extends EventEmitter { 16 | constructor ({ 17 | maximumLength = 500, 18 | triggerTimeInterval = 5000, 19 | // execute on push 20 | proactiveMode = true, 21 | // execute next work on finish 22 | continuousMode = true 23 | }) { 24 | super() 25 | this.max = maximumLength 26 | this.triggerTime = triggerTimeInterval 27 | this.taskMap = new Map() 28 | this.queue = [] 29 | this.lock = false 30 | 31 | this.on(QueueEvent.Tick, this.onEventProcessFunc.bind(this)) 32 | if (proactiveMode) { 33 | this.on(QueueEvent.Push, this.onEventProcessFunc.bind(this)) 34 | } 35 | if (continuousMode) { 36 | this.on(QueueEvent.Finish, this.onEventProcessFunc.bind(this)) 37 | } 38 | } 39 | 40 | onEventProcessFunc () { 41 | if (this.lock) return 42 | this.lock = true 43 | setImmediate(() => { 44 | this.process() 45 | }) 46 | } 47 | 48 | start () { 49 | if (this.eventTrigger) return 50 | this.eventTrigger = setInterval(() => { 51 | this.emit(QueueEvent.Tick) 52 | }, this.triggerTime) 53 | } 54 | 55 | stop () { 56 | if (this.eventTrigger) { 57 | clearInterval(this.eventTrigger) 58 | this.eventTrigger = null 59 | } 60 | } 61 | 62 | checkTaskIsInQueue (id) { 63 | return this.taskMap.has(id) 64 | } 65 | 66 | /** 67 | * pushWithKey a promisify-task to queue 68 | * @param id {string} 69 | * @param processingFunc {Function} 70 | * @returns {boolean} if success return true, otherwise false 71 | */ 72 | push (id, processingFunc) { 73 | if (this.queue.length >= this.max) return false 74 | if (this.checkTaskIsInQueue(id)) return false 75 | const task = { 76 | id: id, 77 | processingFunc: processingFunc 78 | } 79 | this.taskMap.set(id, true) 80 | this.queue.push(task) 81 | this.start() 82 | this.emit(QueueEvent.Push) 83 | return true 84 | } 85 | 86 | process () { 87 | if (this.queue.length <= 0) { 88 | this.stop() 89 | this.lock = false 90 | return 91 | } 92 | 93 | const task = this.queue.shift() 94 | this.taskMap.delete(task.id) 95 | 96 | const finishTask = () => { 97 | this.lock = false 98 | setImmediate(() => { 99 | this.emit(QueueEvent.Finish) 100 | }) 101 | } 102 | task.processingFunc().then(finishTask).catch(finishTask) 103 | } 104 | } 105 | 106 | exports.ProcessQueue = ProcessQueue 107 | -------------------------------------------------------------------------------- /lib/realtime/realtimeCleanDanglingUserJob.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const async = require('async') 4 | const config = require('../config') 5 | const logger = require('../logger') 6 | 7 | /** 8 | * clean when user not in any rooms or user not in connected list 9 | */ 10 | class CleanDanglingUserJob { 11 | constructor (realtime) { 12 | this.realtime = realtime 13 | } 14 | 15 | start () { 16 | if (this.timer) return 17 | this.timer = setInterval(this.cleanDanglingUser.bind(this), 60000) 18 | } 19 | 20 | stop () { 21 | if (!this.timer) return 22 | clearInterval(this.timer) 23 | this.timer = undefined 24 | } 25 | 26 | cleanDanglingUser () { 27 | const users = this.realtime.getUserPool() 28 | async.each(Object.keys(users), (key, callback) => { 29 | const socket = this.realtime.io.sockets.connected[key] 30 | if ((!socket && users[key]) || 31 | (socket && (!socket.rooms || socket.rooms.length <= 0))) { 32 | if (config.debug) { 33 | logger.info('cleaner found redundant user: ' + key) 34 | } 35 | if (!socket) { 36 | return callback(null, null) 37 | } 38 | if (!this.realtime.disconnectProcessQueue.checkTaskIsInQueue(socket.id)) { 39 | this.realtime.queueForDisconnect(socket) 40 | } 41 | } 42 | return callback(null, null) 43 | }, function (err) { 44 | if (err) return logger.error('cleaner error', err) 45 | }) 46 | } 47 | } 48 | 49 | exports.CleanDanglingUserJob = CleanDanglingUserJob 50 | -------------------------------------------------------------------------------- /lib/realtime/realtimeSaveRevisionJob.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const models = require('../models') 4 | const logger = require('../logger') 5 | 6 | /** 7 | * clean when user not in any rooms or user not in connected list 8 | */ 9 | class SaveRevisionJob { 10 | constructor (realtime) { 11 | this.realtime = realtime 12 | this.saverSleep = false 13 | } 14 | 15 | start () { 16 | if (this.timer) return 17 | this.timer = setInterval(this.saveRevision.bind(this), 5 * 60 * 1000) 18 | } 19 | 20 | stop () { 21 | if (!this.timer) return 22 | clearInterval(this.timer) 23 | this.timer = undefined 24 | } 25 | 26 | saveRevision () { 27 | if (this.getSaverSleep()) return 28 | models.Revision.saveAllNotesRevision((err, notes) => { 29 | if (err) return logger.error('revision saver failed: ' + err) 30 | if (notes && notes.length <= 0) { 31 | this.setSaverSleep(true) 32 | } 33 | }) 34 | } 35 | 36 | getSaverSleep () { 37 | return this.saverSleep 38 | } 39 | 40 | setSaverSleep (val) { 41 | this.saverSleep = val 42 | } 43 | } 44 | 45 | exports.SaveRevisionJob = SaveRevisionJob 46 | -------------------------------------------------------------------------------- /lib/realtime/realtimeUpdateDirtyNoteJob.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('../config') 4 | const logger = require('../logger') 5 | const moment = require('moment') 6 | 7 | class UpdateDirtyNoteJob { 8 | constructor (realtime) { 9 | this.realtime = realtime 10 | } 11 | 12 | start () { 13 | if (this.timer) return 14 | this.timer = setInterval(this.updateDirtyNotes.bind(this), 1000) 15 | } 16 | 17 | stop () { 18 | if (!this.timer) return 19 | clearInterval(this.timer) 20 | this.timer = undefined 21 | } 22 | 23 | updateDirtyNotes () { 24 | const notes = this.realtime.getNotePool() 25 | Object.keys(notes).forEach((key) => { 26 | const note = notes[key] 27 | this.updateDirtyNote(note) 28 | .catch((err) => { 29 | logger.error('updateDirtyNote: updater error', err) 30 | }) 31 | }) 32 | } 33 | 34 | async updateDirtyNote (note) { 35 | const notes = this.realtime.getNotePool() 36 | if (!note.server.isDirty) return 37 | 38 | if (config.debug) logger.info('updateDirtyNote: updater found dirty note: ' + note.id) 39 | note.server.isDirty = false 40 | 41 | try { 42 | const _note = await this.updateNoteAsync(note) 43 | // handle when note already been clean up 44 | if (!notes[note.id] || !notes[note.id].server) return 45 | 46 | if (!_note) { 47 | this.realtime.io.to(note.id).emit('info', { 48 | code: 404 49 | }) 50 | logger.error('updateDirtyNote: note not found: ', note.id) 51 | this.realtime.disconnectSocketOnNote(note) 52 | } 53 | 54 | note.updatetime = moment(_note.lastchangeAt).valueOf() 55 | this.realtime.emitCheck(note) 56 | } catch (err) { 57 | logger.error('updateDirtyNote: note not found: ', note.id) 58 | this.realtime.io.to(note.id).emit('info', { 59 | code: 404 60 | }) 61 | this.realtime.disconnectSocketOnNote(note) 62 | throw err 63 | } 64 | } 65 | 66 | updateNoteAsync (note) { 67 | return new Promise((resolve, reject) => { 68 | this.realtime.updateNote(note, (err, _note) => { 69 | if (err) { 70 | return reject(err) 71 | } 72 | return resolve(_note) 73 | }) 74 | }) 75 | } 76 | } 77 | 78 | exports.UpdateDirtyNoteJob = UpdateDirtyNoteJob 79 | -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Router } = require('express') 4 | 5 | const { wrap, urlencodedParser, markdownParser } = require('./utils') 6 | 7 | // load controller 8 | const indexController = require('./homepage') 9 | const errorPageController = require('./errorPage') 10 | const statusController = require('./status') 11 | const historyController = require('./history') 12 | const userController = require('./user') 13 | const noteController = require('./note') 14 | const response = require('./response') 15 | const appRouter = Router() 16 | 17 | // register route 18 | 19 | // get index 20 | appRouter.get('/', wrap(indexController.showIndex)) 21 | 22 | // ----- error page ----- 23 | // get 403 forbidden 24 | appRouter.get('/403', errorPageController.errorForbidden) 25 | // get 404 not found 26 | appRouter.get('/404', errorPageController.errorNotFound) 27 | // get 500 internal error 28 | appRouter.get('/500', errorPageController.errorInternalError) 29 | 30 | appRouter.get('/config', statusController.getConfig) 31 | 32 | // register auth module 33 | appRouter.use(require('./auth')) 34 | 35 | // get history 36 | appRouter.get('/history', historyController.historyGet) 37 | // post history 38 | appRouter.post('/history', urlencodedParser, historyController.historyPost) 39 | // post history by note id 40 | appRouter.post('/history/:noteId', urlencodedParser, historyController.historyPost) 41 | // delete history 42 | appRouter.delete('/history', historyController.historyDelete) 43 | // delete history by note id 44 | appRouter.delete('/history/:noteId', historyController.historyDelete) 45 | 46 | // user 47 | // get me info 48 | appRouter.get('/me', wrap(userController.getMe)) 49 | 50 | // delete the currently authenticated user 51 | appRouter.get('/me/delete/:token?', wrap(userController.deleteUser)) 52 | 53 | // export the data of the authenticated user 54 | appRouter.get('/me/export', userController.exportMyData) 55 | 56 | appRouter.get('/user/:username/avatar.svg', userController.getMyAvatar) 57 | 58 | // register image upload module 59 | appRouter.use(require('./imageRouter')) 60 | 61 | // get new note 62 | appRouter.get('/new', response.newNote) 63 | // post new note with content 64 | appRouter.post('/new', markdownParser, response.newNote) 65 | // get publish note 66 | appRouter.get('/s/:shortid', noteController.showPublishNote) 67 | // publish note actions 68 | appRouter.get('/s/:shortid/:action', response.publishNoteActions) 69 | // get publish slide 70 | appRouter.get('/p/:shortid', response.showPublishSlide) 71 | // publish slide actions 72 | appRouter.get('/p/:shortid/:action', response.publishSlideActions) 73 | // get note by id 74 | appRouter.get('/:noteId', wrap(noteController.showNote)) 75 | // note actions 76 | appRouter.get('/:noteId/:action', noteController.noteActions) 77 | // note actions with action id 78 | appRouter.get('/:noteId/:action/:actionId', noteController.noteActions) 79 | 80 | exports.router = appRouter 81 | -------------------------------------------------------------------------------- /lib/status/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const realtime = require('../realtime/realtime') 4 | const config = require('../config') 5 | 6 | exports.getStatus = async (req, res) => { 7 | res.set({ 8 | 'Cache-Control': 'private', // only cache by client 9 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling 10 | 'Content-Type': 'application/json' 11 | }) 12 | 13 | try { 14 | const data = await realtime.getStatus() 15 | res.send(data) 16 | } catch (e) { 17 | console.error(e) 18 | res.status(500).send(e.toString()) 19 | } 20 | } 21 | 22 | exports.getMetrics = async (req, res) => { 23 | const data = await realtime.getStatus() 24 | 25 | res.set({ 26 | 'Cache-Control': 'private', // only cache by client 27 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling 28 | 'Content-Type': 'text/plain; charset=utf-8' 29 | }) 30 | res.render('../js/lib/common/metrics.ejs', data) 31 | } 32 | 33 | exports.getConfig = (req, res) => { 34 | const data = { 35 | domain: config.domain, 36 | urlpath: config.urlPath, 37 | debug: config.debug, 38 | version: config.fullversion, 39 | plantumlServer: config.plantuml.server, 40 | DROPBOX_APP_KEY: config.dropbox.appKey, 41 | allowedUploadMimeTypes: config.allowedUploadMimeTypes, 42 | defaultUseHardbreak: config.defaultUseHardbreak, 43 | linkifyHeaderStyle: config.linkifyHeaderStyle, 44 | useCDN: config.useCDN, 45 | responsivevoiceKey: config.responsivevoiceKey 46 | } 47 | res.set({ 48 | 'Cache-Control': 'private', // only cache by client 49 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling 50 | 'Content-Type': 'application/javascript' 51 | }) 52 | res.render('../js/lib/common/constant.ejs', data) 53 | } -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const bodyParser = require('body-parser') 5 | 6 | exports.isSQLite = function isSQLite (sequelize) { 7 | return sequelize.options.dialect === 'sqlite' 8 | } 9 | 10 | exports.getImageMimeType = function getImageMimeType (imagePath) { 11 | const fileExtension = /[^.]+$/.exec(imagePath) 12 | 13 | switch (fileExtension[0]) { 14 | case 'bmp': 15 | return 'image/bmp' 16 | case 'gif': 17 | return 'image/gif' 18 | case 'jpg': 19 | case 'jpeg': 20 | return 'image/jpeg' 21 | case 'png': 22 | return 'image/png' 23 | case 'tiff': 24 | return 'image/tiff' 25 | default: 26 | return undefined 27 | } 28 | } 29 | 30 | exports.isRevealTheme = function isRevealTheme (theme) { 31 | if (fs.existsSync(path.join(__dirname, '..', 'public', 'build', 'reveal.js', 'css', 'theme', theme + '.css'))) { 32 | return theme 33 | } 34 | return undefined 35 | } 36 | 37 | exports.wrap = innerHandler => (req, res, next) => innerHandler(req, res).catch(err => next(err)) 38 | 39 | // create application/x-www-form-urlencoded parser 40 | exports.urlencodedParser = bodyParser.urlencoded({ 41 | extended: false, 42 | limit: 1024 * 1024 * 10 // 10 mb 43 | }) 44 | 45 | // create text/markdown parser 46 | exports.markdownParser = bodyParser.text({ 47 | inflate: true, 48 | type: ['text/plain', 'text/markdown'], 49 | limit: 1024 * 1024 * 10 // 10 mb 50 | }) 51 | -------------------------------------------------------------------------------- /lib/web/middleware/checkVersion.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { promisify } = require('util') 4 | 5 | const request = require('request') 6 | 7 | const logger = require('../../logger') 8 | const config = require('../../config') 9 | 10 | let lastCheckAt 11 | 12 | const VERSION_CHECK_ENDPOINT = 'https://evangelion.codimd.dev/' 13 | const CHECK_TIMEOUT = 1000 * 60 * 60 * 24 // 1 day 14 | 15 | const rp = promisify(request) 16 | 17 | exports.checkVersion = checkVersion 18 | /** 19 | * @param {Express.Application|Express.Request} ctx 20 | */ 21 | async function checkVersion (ctx) { 22 | if (lastCheckAt && (lastCheckAt + CHECK_TIMEOUT > Date.now())) { 23 | return 24 | } 25 | 26 | // update lastCheckAt whether the check would fail or not 27 | lastCheckAt = Date.now() 28 | 29 | try { 30 | const { statusCode, body: data } = await rp({ 31 | url: `${VERSION_CHECK_ENDPOINT}?v=${config.version}`, 32 | method: 'GET', 33 | json: true, 34 | timeout: 3000 35 | }) 36 | 37 | if (statusCode !== 200 || data.status === 'error') { 38 | logger.warn('Version check failed.') 39 | return 40 | } 41 | 42 | const locals = ctx.locals ? ctx.locals : ctx.app.locals 43 | 44 | locals.versionInfo.latest = data.latest 45 | locals.versionInfo.versionItem = data.latest ? null : data.versionItem 46 | 47 | if (!data.latest) { 48 | const { version, link } = data.versionItem 49 | 50 | logger.info(`Your CodiLIA version is out of date! The latest version is ${version}. Please see what's new on ${link}.`) 51 | } 52 | } catch (err) { 53 | // ignore and skip version check 54 | logger.warn('Version check failed.') 55 | logger.warn(err) 56 | } 57 | } 58 | 59 | exports.versionCheckMiddleware = function (req, res, next) { 60 | checkVersion(req) 61 | .then(() => { 62 | next() 63 | }) 64 | .catch((err) => { 65 | logger.error(err) 66 | next() 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /public/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // this config file is used in concert with the root .eslintrc.js in the root dir. 2 | module.exports = { 3 | "env": { 4 | "browser": true 5 | }, 6 | "globals": { 7 | "$": false, 8 | "CodeMirror": false, 9 | "Cookies": false, 10 | "moment": false, 11 | "editor": false, 12 | "ui": false, 13 | "modeType": false, 14 | "serverurl": false, 15 | "key": false, 16 | "gapi": false, 17 | "Dropbox": false, 18 | "FilePicker": false, 19 | "ot": false, 20 | "MediaUploader": false, 21 | "hex2rgb": false, 22 | "num_loaded": false, 23 | "Visibility": false, 24 | "inlineAttachment": false 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/codimd-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/codimd-icon-1024.png -------------------------------------------------------------------------------- /public/css/center.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | .container-fluid { 4 | height: 98%; 5 | } 6 | .container-fluid { 7 | display: table; 8 | vertical-align: middle; 9 | } 10 | .vertical-center-row { 11 | display: table-cell; 12 | vertical-align: middle; 13 | } -------------------------------------------------------------------------------- /public/css/codemirror-extend/ayu-dark.css: -------------------------------------------------------------------------------- 1 | /* Based on https://github.com/dempfi/ayu */ 2 | 3 | .cm-s-ayu-dark.CodeMirror { background: #0a0e14; color: #b3b1ad; } 4 | .cm-s-ayu-dark div.CodeMirror-selected { background: #273747; } 5 | .cm-s-ayu-dark .CodeMirror-line::selection, .cm-s-ayu-dark .CodeMirror-line > span::selection, .cm-s-ayu-dark .CodeMirror-line > span > span::selection { background: rgba(39, 55, 71, 99); } 6 | .cm-s-ayu-dark .CodeMirror-line::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 55, 71, 99); } 7 | .cm-s-ayu-dark .CodeMirror-gutters { background: #0a0e14; border-right: 0px; } 8 | .cm-s-ayu-dark .CodeMirror-guttermarker { color: white; } 9 | .cm-s-ayu-dark .CodeMirror-guttermarker-subtle { color: #3d424d; } 10 | .cm-s-ayu-dark .CodeMirror-linenumber { color: #3d424d; } 11 | .cm-s-ayu-dark .CodeMirror-cursor { border-left: 1px solid #e6b450; } 12 | 13 | .cm-s-ayu-dark span.cm-comment { color: #626a73; } 14 | .cm-s-ayu-dark span.cm-atom { color: #ae81ff; } 15 | .cm-s-ayu-dark span.cm-number { color: #e6b450; } 16 | 17 | .cm-s-ayu-dark span.cm-comment.cm-attribute { color: #ffb454; } 18 | .cm-s-ayu-dark span.cm-comment.cm-def { color: rgba(57, 186, 230, 80); } 19 | .cm-s-ayu-dark span.cm-comment.cm-tag { color: #39bae6; } 20 | .cm-s-ayu-dark span.cm-comment.cm-type { color: #5998a6; } 21 | 22 | .cm-s-ayu-dark span.cm-property, .cm-s-ayu-dark span.cm-attribute { color: #ffb454; } 23 | .cm-s-ayu-dark span.cm-keyword { color: #ff8f40; } 24 | .cm-s-ayu-dark span.cm-builtin { color: #e6b450; } 25 | .cm-s-ayu-dark span.cm-string { color: #c2d94c; } 26 | 27 | .cm-s-ayu-dark span.cm-variable { color: #b3b1ad; } 28 | .cm-s-ayu-dark span.cm-variable-2 { color: #f07178; } 29 | .cm-s-ayu-dark span.cm-variable-3 { color: #39bae6; } 30 | .cm-s-ayu-dark span.cm-type { color: #ff8f40; } 31 | .cm-s-ayu-dark span.cm-def { color: #ffee99; } 32 | .cm-s-ayu-dark span.cm-bracket { color: #f8f8f2; } 33 | .cm-s-ayu-dark span.cm-tag { color: rgba(57, 186, 230, 80); } 34 | .cm-s-ayu-dark span.cm-header { color: #c2d94c; } 35 | .cm-s-ayu-dark span.cm-link { color: #39bae6; } 36 | .cm-s-ayu-dark span.cm-error { color: #ff3333; } 37 | 38 | .cm-s-ayu-dark .CodeMirror-activeline-background { background: #01060e; } 39 | .cm-s-ayu-dark .CodeMirror-matchingbracket { 40 | text-decoration: underline; 41 | color: white !important; 42 | } 43 | -------------------------------------------------------------------------------- /public/css/codemirror-extend/ayu-mirage.css: -------------------------------------------------------------------------------- 1 | /* Based on https://github.com/dempfi/ayu */ 2 | 3 | .cm-s-ayu-mirage.CodeMirror { background: #1f2430; color: #cbccc6; } 4 | .cm-s-ayu-mirage div.CodeMirror-selected { background: #34455a; } 5 | .cm-s-ayu-mirage .CodeMirror-line::selection, .cm-s-ayu-mirage .CodeMirror-line > span::selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::selection { background: #34455a; } 6 | .cm-s-ayu-mirage .CodeMirror-line::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::-moz-selection { background: rgba(25, 30, 42, 99); } 7 | .cm-s-ayu-mirage .CodeMirror-gutters { background: #1f2430; border-right: 0px; } 8 | .cm-s-ayu-mirage .CodeMirror-guttermarker { color: white; } 9 | .cm-s-ayu-mirage .CodeMirror-guttermarker-subtle { color: rgba(112, 122, 140, 66); } 10 | .cm-s-ayu-mirage .CodeMirror-linenumber { color: rgba(61, 66, 77, 99); } 11 | .cm-s-ayu-mirage .CodeMirror-cursor { border-left: 1px solid #ffcc66; } 12 | 13 | .cm-s-ayu-mirage span.cm-comment { color: #5c6773; font-style:italic; } 14 | .cm-s-ayu-mirage span.cm-atom { color: #ae81ff; } 15 | .cm-s-ayu-mirage span.cm-number { color: #ffcc66; } 16 | 17 | .cm-s-ayu-mirage span.cm-comment.cm-attribute { color: #ffd580; } 18 | .cm-s-ayu-mirage span.cm-comment.cm-def { color: #d4bfff; } 19 | .cm-s-ayu-mirage span.cm-comment.cm-tag { color: #5ccfe6; } 20 | .cm-s-ayu-mirage span.cm-comment.cm-type { color: #5998a6; } 21 | 22 | .cm-s-ayu-mirage span.cm-property { color: #f29e74; } 23 | .cm-s-ayu-mirage span.cm-attribute { color: #ffd580; } 24 | .cm-s-ayu-mirage span.cm-keyword { color: #ffa759; } 25 | .cm-s-ayu-mirage span.cm-builtin { color: #ffcc66; } 26 | .cm-s-ayu-mirage span.cm-string { color: #bae67e; } 27 | 28 | .cm-s-ayu-mirage span.cm-variable { color: #cbccc6; } 29 | .cm-s-ayu-mirage span.cm-variable-2 { color: #f28779; } 30 | .cm-s-ayu-mirage span.cm-variable-3 { color: #5ccfe6; } 31 | .cm-s-ayu-mirage span.cm-type { color: #ffa759; } 32 | .cm-s-ayu-mirage span.cm-def { color: #ffd580; } 33 | .cm-s-ayu-mirage span.cm-bracket { color: rgba(92, 207, 230, 80); } 34 | .cm-s-ayu-mirage span.cm-tag { color: #5ccfe6; } 35 | .cm-s-ayu-mirage span.cm-header { color: #bae67e; } 36 | .cm-s-ayu-mirage span.cm-link { color: #5ccfe6; } 37 | .cm-s-ayu-mirage span.cm-error { color: #ff3333; } 38 | 39 | .cm-s-ayu-mirage .CodeMirror-activeline-background { background: #191e2a; } 40 | .cm-s-ayu-mirage .CodeMirror-matchingbracket { 41 | text-decoration: underline; 42 | color: white !important; 43 | } 44 | -------------------------------------------------------------------------------- /public/css/codemirror-extend/tomorrow-night-bright.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Name: Tomorrow Night - Bright 4 | Author: Chris Kempson 5 | 6 | Port done by Gerard Braad 7 | 8 | */ 9 | 10 | .cm-s-tomorrow-night-bright.CodeMirror { background: #000000; color: #eaeaea; } 11 | .cm-s-tomorrow-night-bright div.CodeMirror-selected { background: #424242; } 12 | .cm-s-tomorrow-night-bright .CodeMirror-gutters { background: #000000; border-right: 0px; } 13 | .cm-s-tomorrow-night-bright .CodeMirror-guttermarker { color: #e78c45; } 14 | .cm-s-tomorrow-night-bright .CodeMirror-guttermarker-subtle { color: #777; } 15 | .cm-s-tomorrow-night-bright .CodeMirror-linenumber { color: #424242; } 16 | .cm-s-tomorrow-night-bright .CodeMirror-cursor { border-left: 1px solid #6A6A6A; } 17 | 18 | .cm-s-tomorrow-night-bright span.cm-comment { color: #d27b53; } 19 | .cm-s-tomorrow-night-bright span.cm-atom { color: #a16a94; } 20 | .cm-s-tomorrow-night-bright span.cm-number { color: #a16a94; } 21 | 22 | .cm-s-tomorrow-night-bright span.cm-property, .cm-s-tomorrow-night-bright span.cm-attribute { color: #99cc99; } 23 | .cm-s-tomorrow-night-bright span.cm-keyword { color: #d54e53; } 24 | .cm-s-tomorrow-night-bright span.cm-string { color: #e7c547; } 25 | 26 | .cm-s-tomorrow-night-bright span.cm-variable { color: #b9ca4a; } 27 | .cm-s-tomorrow-night-bright span.cm-variable-2 { color: #7aa6da; } 28 | .cm-s-tomorrow-night-bright span.cm-def { color: #e78c45; } 29 | .cm-s-tomorrow-night-bright span.cm-bracket { color: #eaeaea; } 30 | .cm-s-tomorrow-night-bright span.cm-tag { color: #d54e53; } 31 | .cm-s-tomorrow-night-bright span.cm-link { color: #a16a94; } 32 | .cm-s-tomorrow-night-bright span.cm-error { background: #d54e53; color: #6A6A6A; } 33 | 34 | .cm-s-tomorrow-night-bright .CodeMirror-activeline-background { background: #2a2a2a; } 35 | .cm-s-tomorrow-night-bright .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; } 36 | -------------------------------------------------------------------------------- /public/css/codemirror-extend/tomorrow-night-eighties.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Name: Tomorrow Night - Eighties 4 | Author: Chris Kempson 5 | 6 | CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror) 7 | Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) 8 | 9 | */ 10 | 11 | .cm-s-tomorrow-night-eighties.CodeMirror { background: #000000; color: #CCCCCC; } 12 | .cm-s-tomorrow-night-eighties div.CodeMirror-selected { background: #2D2D2D; } 13 | .cm-s-tomorrow-night-eighties .CodeMirror-line::selection, .cm-s-tomorrow-night-eighties .CodeMirror-line > span::selection, .cm-s-tomorrow-night-eighties .CodeMirror-line > span > span::selection { background: rgba(45, 45, 45, 0.99); } 14 | .cm-s-tomorrow-night-eighties .CodeMirror-line::-moz-selection, .cm-s-tomorrow-night-eighties .CodeMirror-line > span::-moz-selection, .cm-s-tomorrow-night-eighties .CodeMirror-line > span > span::-moz-selection { background: rgba(45, 45, 45, 0.99); } 15 | .cm-s-tomorrow-night-eighties .CodeMirror-gutters { background: #000000; border-right: 0px; } 16 | .cm-s-tomorrow-night-eighties .CodeMirror-guttermarker { color: #f2777a; } 17 | .cm-s-tomorrow-night-eighties .CodeMirror-guttermarker-subtle { color: #777; } 18 | .cm-s-tomorrow-night-eighties .CodeMirror-linenumber { color: #515151; } 19 | .cm-s-tomorrow-night-eighties .CodeMirror-cursor { border-left: 1px solid #6A6A6A; } 20 | 21 | .cm-s-tomorrow-night-eighties span.cm-comment { color: #d27b53; } 22 | .cm-s-tomorrow-night-eighties span.cm-atom { color: #a16a94; } 23 | .cm-s-tomorrow-night-eighties span.cm-number { color: #a16a94; } 24 | 25 | .cm-s-tomorrow-night-eighties span.cm-property, .cm-s-tomorrow-night-eighties span.cm-attribute { color: #99cc99; } 26 | .cm-s-tomorrow-night-eighties span.cm-keyword { color: #f2777a; } 27 | .cm-s-tomorrow-night-eighties span.cm-string { color: #ffcc66; } 28 | 29 | .cm-s-tomorrow-night-eighties span.cm-variable { color: #99cc99; } 30 | .cm-s-tomorrow-night-eighties span.cm-variable-2 { color: #6699cc; } 31 | .cm-s-tomorrow-night-eighties span.cm-def { color: #f99157; } 32 | .cm-s-tomorrow-night-eighties span.cm-bracket { color: #CCCCCC; } 33 | .cm-s-tomorrow-night-eighties span.cm-tag { color: #f2777a; } 34 | .cm-s-tomorrow-night-eighties span.cm-link { color: #a16a94; } 35 | .cm-s-tomorrow-night-eighties span.cm-error { background: #f2777a; color: #6A6A6A; } 36 | 37 | .cm-s-tomorrow-night-eighties .CodeMirror-activeline-background { background: #343600; } 38 | .cm-s-tomorrow-night-eighties .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; } 39 | -------------------------------------------------------------------------------- /public/css/google-font.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,600italic,300italic,300|Source+Serif+Pro|Source+Code+Pro:400,300,500&subset=latin,latin-ext); 2 | -------------------------------------------------------------------------------- /public/css/site.css: -------------------------------------------------------------------------------- 1 | /* for all pages should include this */ 2 | body { 3 | font-smoothing: subpixel-antialiased !important; 4 | -webkit-font-smoothing: subpixel-antialiased !important; 5 | -moz-osx-font-smoothing: auto !important; 6 | text-shadow: 0 0 1em transparent, 1px 1px 1.2px rgba(0, 0, 0, 0.004); 7 | /*text-rendering: optimizeLegibility;*/ 8 | -webkit-overflow-scrolling: touch; 9 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif; 10 | letter-spacing: 0.025em; 11 | } 12 | :focus, .focus { 13 | outline: none !important; 14 | } 15 | ::-moz-focus-inner { 16 | border: 0 !important; 17 | } 18 | 19 | /* manual fix for bootstrap issue 14040, there is an unnecessary padding-right on modal open */ 20 | body.modal-open { 21 | overflow-y: auto; 22 | padding-right: 0 !important; 23 | } 24 | -------------------------------------------------------------------------------- /public/css/slide-preview.css: -------------------------------------------------------------------------------- 1 | .markdown-body.slides { 2 | position: relative; 3 | z-index: 1; 4 | color: #222; 5 | } 6 | 7 | .markdown-body.slides::before { 8 | content: ''; 9 | display: block; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | z-index: -1; 16 | background-color: currentColor; 17 | box-shadow: 0 0 0 50vw; 18 | } 19 | 20 | .markdown-body.slides section[data-markdown] { 21 | position: relative; 22 | margin-bottom: 1.5em; 23 | background-color: #fff; 24 | text-align: center; 25 | } 26 | 27 | .markdown-body.slides section[data-markdown] code { 28 | text-align: left; 29 | } 30 | 31 | .markdown-body.slides section[data-markdown]::before { 32 | content: ''; 33 | display: block; 34 | padding-bottom: 56.23%; 35 | } 36 | 37 | .markdown-body.slides section[data-markdown] > div:first-child { 38 | position: absolute; 39 | top: 50%; 40 | left: 1em; 41 | right: 1em; 42 | transform: translateY(-50%); 43 | max-height: 100%; 44 | overflow: hidden; 45 | } 46 | 47 | .markdown-body.slides section[data-markdown] > ul { 48 | display: inline-block; 49 | } 50 | 51 | .markdown-body.slides > section > section + section::after { 52 | content: ''; 53 | position: absolute; 54 | top: -1.5em; 55 | right: 1em; 56 | height: 1.5em; 57 | border: 3px solid #777; 58 | } 59 | -------------------------------------------------------------------------------- /public/default.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/default.md -------------------------------------------------------------------------------- /public/docs/privacy.md.example: -------------------------------------------------------------------------------- 1 | Privacy 2 | === 3 | 4 | We process the following data, for the following purposes: 5 | 6 | |your data|our usage| 7 | |---------|---------| 8 | |IP-Address|Used to communicate with your browser and our servers. It's may exposed to third-parties which provide resources for this service. These services are, depending on your login method, the document you visit and the setup of this instance: Google, Disqus, MathJax, GitHub, SlideShare/LinkedIn, yahoo, Gravatar, Imgur, Amazon, and Cloudflare.| 9 | |Usernames and profiles|Your username as well as user profiles that are connected with it are transmitted and stored by us to provide a useful login integration with services like GitHub, Facebook, Twitter, GitLab, Dropbox, Google. Depending on the setup of this CodiMD instance there are maybe other third-parties involved using SAML, LDAP or the integration with a Mattermost instance.| 10 | |Profile pictures| Your profile picture is either loaded from the service you used to login, the CodiMD instance or Gravatar.| 11 | |Uploaded pictures| Pictures that are uploaded for documents are either uploaded to Amazon S3, Imgur, a minio instance or the local filesystem of the CodiMD server.| 12 | 13 | All account data and notes are stored in a mysql/postgres/sqlite database. Besides the user accounts and the document themselves also relationships between the documents and the user accounts are stored. This includes ownership, authorship and revisions of all changes made during the creation of a note. 14 | 15 | To delete your account and all your notes owned by your user account, you can find a button in the drop down menu on the front page. 16 | 17 | The deletion of guest notes is not possible. These don't have any ownership and this means we can't connect these to you or anyone else. If you participated in a guest note or a note owned by someone else, your authorship for the revisions is removed from these notes as well. But the content you created will stay in place as the integrity of these notes has to stay untouched. 18 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Black.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Black.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Black.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Bold.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Bold.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-ExtraLight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-ExtraLight.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-ExtraLight.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-ExtraLight.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Light.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Light.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Light.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Medium.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Medium.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Medium.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Medium.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Regular.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Regular.woff -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Semibold.eot -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Semibold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceCodePro-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceCodePro-Semibold.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Black.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Black.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Black.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BlackItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-BlackItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-BlackItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BlackItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-BlackItalic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Bold.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Bold.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BoldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-BoldItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-BoldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-BoldItalic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLight.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-ExtraLight.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-ExtraLight.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-ExtraLight.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-ExtraLightItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-ExtraLightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-ExtraLightItalic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Italic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Italic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Italic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Light.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Light.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Light.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-LightItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-LightItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-LightItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-LightItalic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Regular.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Regular.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Semibold.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Semibold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-Semibold.woff -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-SemiboldItalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-SemiboldItalic.eot -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-SemiboldItalic.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSansPro-SemiboldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSansPro-SemiboldItalic.woff -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Bold.eot -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Bold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Bold.woff -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Regular.eot -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Regular.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Regular.woff -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Semibold.eot -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Semibold.ttf -------------------------------------------------------------------------------- /public/fonts/SourceSerifPro-Semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/fonts/SourceSerifPro-Semibold.woff -------------------------------------------------------------------------------- /public/images/mattermost-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 33 | 34 | -------------------------------------------------------------------------------- /public/js/htmlExport.js: -------------------------------------------------------------------------------- 1 | require('../css/github-extract.css') 2 | require('../css/markdown.css') 3 | require('../css/extra.css') 4 | require('../css/slide-preview.css') 5 | require('../css/google-font.css') 6 | require('../css/site.css') 7 | -------------------------------------------------------------------------------- /public/js/lib/appState.js: -------------------------------------------------------------------------------- 1 | import modeType from './modeType' 2 | 3 | const state = { 4 | syncscroll: true, 5 | currentMode: modeType.view, 6 | nightMode: false 7 | } 8 | 9 | export default state 10 | -------------------------------------------------------------------------------- /public/js/lib/common/constant.ejs: -------------------------------------------------------------------------------- 1 | window.domain = '<%- domain %>' 2 | window.urlpath = '<%- urlpath %>' 3 | window.debug = <%- debug %> 4 | window.version = '<%- version %>' 5 | window.plantumlServer = '<%- plantumlServer %>' 6 | 7 | window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %> 8 | 9 | window.defaultUseHardbreak = <%- defaultUseHardbreak %> 10 | 11 | window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>' 12 | 13 | window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>' 14 | 15 | window.USE_CDN = <%- useCDN %> 16 | -------------------------------------------------------------------------------- /public/js/lib/common/login.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, jquery */ 2 | /* global Cookies */ 3 | 4 | import { serverurl } from '../config' 5 | 6 | let checkAuth = false 7 | let profile = null 8 | let lastLoginState = getLoginState() 9 | let lastUserId = getUserId() 10 | var loginStateChangeEvent = null 11 | 12 | export function setloginStateChangeEvent (func) { 13 | loginStateChangeEvent = func 14 | } 15 | 16 | export function resetCheckAuth () { 17 | checkAuth = false 18 | } 19 | 20 | export function setLoginState (bool, id) { 21 | Cookies.set('loginstate', bool, { 22 | expires: 365 23 | }) 24 | if (id) { 25 | Cookies.set('userid', id, { 26 | expires: 365 27 | }) 28 | } else { 29 | Cookies.remove('userid') 30 | } 31 | lastLoginState = bool 32 | lastUserId = id 33 | checkLoginStateChanged() 34 | } 35 | 36 | export function checkLoginStateChanged () { 37 | if (getLoginState() !== lastLoginState || getUserId() !== lastUserId) { 38 | if (loginStateChangeEvent) setTimeout(loginStateChangeEvent, 100) 39 | return true 40 | } else { 41 | return false 42 | } 43 | } 44 | 45 | export function getLoginState () { 46 | const state = Cookies.get('loginstate') 47 | return state === 'true' || state === true 48 | } 49 | 50 | export function getUserId () { 51 | return Cookies.get('userid') 52 | } 53 | 54 | export function clearLoginState () { 55 | Cookies.remove('loginstate') 56 | } 57 | 58 | export function checkIfAuth (yesCallback, noCallback) { 59 | const cookieLoginState = getLoginState() 60 | if (checkLoginStateChanged()) checkAuth = false 61 | if (!checkAuth || typeof cookieLoginState === 'undefined') { 62 | $.get(`${serverurl}/me`) 63 | .done(data => { 64 | if (data && data.status === 'ok') { 65 | profile = data 66 | yesCallback(profile) 67 | setLoginState(true, data.id) 68 | } else { 69 | noCallback() 70 | setLoginState(false) 71 | } 72 | }) 73 | .fail(() => { 74 | noCallback() 75 | }) 76 | .always(() => { 77 | checkAuth = true 78 | }) 79 | } else if (cookieLoginState) { 80 | yesCallback(profile) 81 | } else { 82 | noCallback() 83 | } 84 | } 85 | 86 | export default { 87 | checkAuth, 88 | profile, 89 | lastLoginState, 90 | lastUserId, 91 | loginStateChangeEvent 92 | } 93 | -------------------------------------------------------------------------------- /public/js/lib/common/metrics.ejs: -------------------------------------------------------------------------------- 1 | online_notes <%- onlineNotes %> 2 | online_users <%- onlineUsers %> 3 | distinct_online_users <%- distinctOnlineUsers %> 4 | notes_count <%- notesCount %> 5 | registered_users <%- registeredUsers %> 6 | online_registered_users <%- onlineRegisteredUsers %> 7 | distinct_online_registered_users <%- distinctOnlineRegisteredUsers %> 8 | is_connection_busy <%- isConnectionBusy ? 1 : 0 %> 9 | connection_socket_queue_length <%- connectionSocketQueueLength %> 10 | is_disconnect_busy <%- isDisconnectBusy ? 1: 0 %> 11 | disconnect_socket_queue_length <%- disconnectSocketQueueLength %> 12 | -------------------------------------------------------------------------------- /public/js/lib/config/index.js: -------------------------------------------------------------------------------- 1 | export const DROPBOX_APP_KEY = window.DROPBOX_APP_KEY || '' 2 | 3 | export const domain = window.domain || '' // domain name 4 | export const urlpath = window.urlpath || '' // sub url path, like: www.example.com/ 5 | export const debug = window.debug || false 6 | 7 | export const port = window.location.port 8 | export const serverurl = `${window.location.protocol}//${domain || window.location.hostname}${port ? ':' + port : ''}${urlpath ? '/' + urlpath : ''}` 9 | window.serverurl = serverurl 10 | export const noteid = decodeURIComponent(urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1]) 11 | export const noteurl = `${serverurl}/${noteid}` 12 | 13 | export const version = window.version 14 | -------------------------------------------------------------------------------- /public/js/lib/editor/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | docmaxlength: null 3 | } 4 | 5 | export default config 6 | -------------------------------------------------------------------------------- /public/js/lib/editor/constants.js: -------------------------------------------------------------------------------- 1 | import { serverurl } from '../config' 2 | 3 | export const availableThemes = [ 4 | { name: 'Light', value: 'default' }, 5 | { name: 'One Dark (Default)', value: 'one-dark' }, 6 | { name: 'Monokai', value: 'monokai' }, 7 | { name: 'Solarized Dark', value: 'solarized dark' }, 8 | { name: 'Solarized Light', value: 'solarized light' }, 9 | { name: 'Dracula', value: 'dracula' }, 10 | { name: 'Material', value: 'material' }, 11 | { name: 'Nord', value: 'nord' }, 12 | { name: 'Panda', value: 'panda-syntax' }, 13 | { name: 'Ayu Dark', value: 'ayu-dark' }, 14 | { name: 'Ayu Mirage', value: 'ayu-mirage' }, 15 | { name: 'Tomorror Night Bright', value: 'tomorrow-night-bright' }, 16 | { name: 'Tomorror Night Eighties', value: 'tomorrow-night-eighties' } 17 | ] 18 | 19 | export const emojifyImageDir = window.USE_CDN ? `https://cdn.jsdelivr.net/npm/@hackmd/emojify.js@2.1.0/dist/images/basic` : `${serverurl}/build/emojify.js/dist/images/basic` 20 | -------------------------------------------------------------------------------- /public/js/lib/editor/markdown-lint/index.js: -------------------------------------------------------------------------------- 1 | /* global CodeMirror */ 2 | 3 | // load CM lint plugin explicitly 4 | import '@hackmd/codemirror/addon/lint/lint' 5 | 6 | window.markdownit = require('markdown-it') 7 | // eslint-disable-next-line 8 | require('script-loader!markdownlint'); 9 | 10 | (function (mod) { 11 | mod(CodeMirror) 12 | })(function (CodeMirror) { 13 | function validator (text) { 14 | return lint(text).map(error => { 15 | const { 16 | ruleNames, 17 | ruleDescription, 18 | lineNumber: ln, 19 | errorRange 20 | } = error 21 | const lineNumber = ln - 1 22 | 23 | let start = 0; let end = -1 24 | if (errorRange) { 25 | [start, end] = errorRange.map(r => r - 1) 26 | } 27 | 28 | return { 29 | messageHTML: `${ruleNames.join('/')}: ${ruleDescription}`, 30 | severity: 'error', 31 | from: CodeMirror.Pos(lineNumber, start), 32 | to: CodeMirror.Pos(lineNumber, end) 33 | } 34 | }) 35 | } 36 | 37 | CodeMirror.registerHelper('lint', 'markdown', validator) 38 | }) 39 | 40 | function lint (content) { 41 | const { content: errors } = window.markdownlint.sync({ 42 | strings: { 43 | content 44 | } 45 | }) 46 | return errors 47 | } 48 | -------------------------------------------------------------------------------- /public/js/lib/markdown/utils.js: -------------------------------------------------------------------------------- 1 | export function parseFenceCodeParams (lang) { 2 | const attrMatch = lang.match(/{(.*)}/) 3 | const params = {} 4 | if (attrMatch && attrMatch.length >= 2) { 5 | const attrs = attrMatch[1] 6 | const paraMatch = attrs.match(/([#.](\S+?)\s)|((\S+?)\s*=\s*("(.+?)"|'(.+?)'|\[[^\]]*\]|\{[}]*\}|(\S+)))/g) 7 | paraMatch && paraMatch.forEach(param => { 8 | param = param.trim() 9 | if (param[0] === '#') { 10 | params['id'] = param.slice(1) 11 | } else if (param[0] === '.') { 12 | if (params['class']) params['class'] = [] 13 | params['class'] = params['class'].concat(param.slice(1)) 14 | } else { 15 | const offset = param.indexOf('=') 16 | const id = param.substring(0, offset).trim().toLowerCase() 17 | let val = param.substring(offset + 1).trim() 18 | const valStart = val[0] 19 | const valEnd = val[val.length - 1] 20 | if (['"', "'"].indexOf(valStart) !== -1 && ['"', "'"].indexOf(valEnd) !== -1 && valStart === valEnd) { 21 | val = val.substring(1, val.length - 1) 22 | } 23 | if (id === 'class') { 24 | if (params['class']) params['class'] = [] 25 | params['class'] = params['class'].concat(val) 26 | } else { 27 | params[id] = val 28 | } 29 | } 30 | }) 31 | } 32 | return params 33 | } 34 | 35 | export function serializeParamToAttribute (params) { 36 | if (Object.getOwnPropertyNames(params).length === 0) { 37 | return '' 38 | } else { 39 | return ` data-params="${escape(JSON.stringify(params))}"` 40 | } 41 | } 42 | 43 | /** 44 | * @param {HTMLElement} elem 45 | */ 46 | export function deserializeParamAttributeFromElement (elem) { 47 | const params = elem.getAttribute('data-params') 48 | if (params) { 49 | return JSON.parse(unescape(params)) 50 | } else { 51 | return {} 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/js/lib/modeType.js: -------------------------------------------------------------------------------- 1 | export default { 2 | edit: { 3 | name: 'edit' 4 | }, 5 | view: { 6 | name: 'view' 7 | }, 8 | both: { 9 | name: 'both' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/js/locale.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, jquery */ 2 | /* global Cookies */ 3 | 4 | var lang = 'en' 5 | var userLang = navigator.language || navigator.userLanguage 6 | var userLangCode = userLang.split('-')[0] 7 | var locale = $('.ui-locale') 8 | var supportLangs = [] 9 | $('.ui-locale option').each(function () { 10 | supportLangs.push($(this).val()) 11 | }) 12 | if (Cookies.get('locale')) { 13 | lang = Cookies.get('locale') 14 | if (lang === 'zh') { 15 | lang = 'zh-TW' 16 | } 17 | } else if (supportLangs.indexOf(userLang) !== -1) { 18 | lang = supportLangs[supportLangs.indexOf(userLang)] 19 | } else if (supportLangs.indexOf(userLangCode) !== -1) { 20 | lang = supportLangs[supportLangs.indexOf(userLangCode)] 21 | } 22 | 23 | locale.val(lang) 24 | $('select.ui-locale option[value="' + lang + '"]').attr('selected', 'selected') 25 | 26 | locale.change(function () { 27 | Cookies.set('locale', $(this).val(), { 28 | expires: 365 29 | }) 30 | window.location.reload() 31 | }) 32 | -------------------------------------------------------------------------------- /public/js/utils.js: -------------------------------------------------------------------------------- 1 | import base64url from 'base64url' 2 | 3 | const 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 4 | 5 | export function checkNoteIdValid (id) { 6 | const result = id.match(uuidRegex) 7 | return !!(result && result.length === 1) 8 | } 9 | 10 | export function encodeNoteId (id) { 11 | // remove dashes in UUID and encode in url-safe base64 12 | const str = id.replace(/-/g, '') 13 | const hexStr = Buffer.from(str, 'hex') 14 | return base64url.encode(hexStr) 15 | } 16 | 17 | export function decodeNoteId (encodedId) { 18 | // decode from url-safe base64 19 | const id = base64url.toBuffer(encodedId).toString('hex') 20 | // add dashes between the UUID string parts 21 | const idParts = [] 22 | idParts.push(id.substr(0, 8)) 23 | idParts.push(id.substr(8, 4)) 24 | idParts.push(id.substr(12, 4)) 25 | idParts.push(id.substr(16, 4)) 26 | idParts.push(id.substr(20, 12)) 27 | return idParts.join('-') 28 | } 29 | -------------------------------------------------------------------------------- /public/markdown-lint/css/lint.css: -------------------------------------------------------------------------------- 1 | /* The lint marker gutter */ 2 | .CodeMirror-lint-markers { 3 | width: 16px; 4 | } 5 | 6 | .CodeMirror-lint-tooltip { 7 | background-color: #333333; 8 | border: 1px solid #eeeeee; 9 | border-radius: 4px; 10 | color: white; 11 | font-family: "Source Code Pro", Consolas, monaco, monospace; 12 | font-size: 10pt; 13 | overflow: hidden; 14 | padding: 2px 5px; 15 | position: fixed; 16 | white-space: pre; 17 | white-space: pre-wrap; 18 | z-index: 100; 19 | max-width: 600px; 20 | opacity: 0; 21 | transition: opacity .4s; 22 | -moz-transition: opacity .4s; 23 | -webkit-transition: opacity .4s; 24 | -o-transition: opacity .4s; 25 | -ms-transition: opacity .4s; 26 | } 27 | 28 | .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { 29 | background-position: left bottom; 30 | background-repeat: repeat-x; 31 | } 32 | 33 | .CodeMirror-lint-mark-error { 34 | background-image: url(../images/mark-error.png); 35 | } 36 | 37 | .CodeMirror-lint-mark-warning { 38 | background-image: url(../images/mark-warning.png); 39 | } 40 | 41 | .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { 42 | background-position: center center; 43 | background-repeat: no-repeat; 44 | cursor: pointer; 45 | display: inline-block; 46 | height: 16px; 47 | width: 16px; 48 | vertical-align: middle; 49 | position: relative; 50 | margin-left: 5px; 51 | } 52 | 53 | .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { 54 | padding-left: 20px; 55 | background-position: top left; 56 | background-repeat: no-repeat; 57 | background-position-y: 2px; 58 | } 59 | 60 | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { 61 | background-image: url(../images/message-error.png); 62 | } 63 | 64 | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { 65 | background-image: url(../images/message-warning.png); 66 | } 67 | 68 | .CodeMirror-lint-marker-multiple { 69 | background-image: url(../images/mark-multiple.png); 70 | background-repeat: no-repeat; 71 | background-position: right bottom; 72 | width: 100%; height: 100%; 73 | } 74 | -------------------------------------------------------------------------------- /public/markdown-lint/images/mark-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/markdown-lint/images/mark-error.png -------------------------------------------------------------------------------- /public/markdown-lint/images/mark-multiple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/markdown-lint/images/mark-multiple.png -------------------------------------------------------------------------------- /public/markdown-lint/images/mark-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/markdown-lint/images/mark-warning.png -------------------------------------------------------------------------------- /public/markdown-lint/images/message-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/markdown-lint/images/message-error.png -------------------------------------------------------------------------------- /public/markdown-lint/images/message-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/markdown-lint/images/message-warning.png -------------------------------------------------------------------------------- /public/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/screenshot.gif -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/screenshot.png -------------------------------------------------------------------------------- /public/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/uploads/.gitkeep -------------------------------------------------------------------------------- /public/vendor/codemirror-spell-checker/spell-checker.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * codemirror-spell-checker v1.0.6 3 | * Copyright Next Step Webs, Inc. 4 | * @link https://github.com/NextStepWebs/codemirror-spell-checker 5 | * @license MIT 6 | */ 7 | .CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){border-bottom:2px dotted rgba(255,0,0,.8)} -------------------------------------------------------------------------------- /public/vendor/inlineAttachment/codemirror.inline-attachment.js: -------------------------------------------------------------------------------- 1 | /*jslint newcap: true */ 2 | /*global inlineAttachment: false */ 3 | /** 4 | * CodeMirror version for inlineAttachment 5 | * 6 | * Call inlineAttachment.attach(editor) to attach to a codemirror instance 7 | */ 8 | (function() { 9 | 'use strict'; 10 | 11 | var codeMirrorEditor = function(instance) { 12 | 13 | if (!instance.getWrapperElement) { 14 | throw "Invalid CodeMirror object given"; 15 | } 16 | 17 | this.codeMirror = instance; 18 | }; 19 | 20 | codeMirrorEditor.prototype.getValue = function() { 21 | return this.codeMirror.getValue(); 22 | }; 23 | 24 | codeMirrorEditor.prototype.insertValue = function(val) { 25 | this.codeMirror.replaceSelection(val); 26 | }; 27 | 28 | codeMirrorEditor.prototype.setValue = function(val) { 29 | var cursor = this.codeMirror.getCursor(); 30 | this.codeMirror.setValue(val); 31 | this.codeMirror.setCursor(cursor); 32 | }; 33 | 34 | codeMirrorEditor.prototype.replaceRange = function(val) { 35 | this.codeMirror.replaceRange(val.replacement, val.from, val.to, "+input"); 36 | }; 37 | 38 | /** 39 | * Attach InlineAttachment to CodeMirror 40 | * 41 | * @param {CodeMirror} codeMirror 42 | */ 43 | codeMirrorEditor.attach = function(codeMirror, options) { 44 | 45 | options = options || {}; 46 | 47 | var editor = new codeMirrorEditor(codeMirror), 48 | inlineattach = new inlineAttachment(options, editor), 49 | el = codeMirror.getWrapperElement(); 50 | 51 | el.addEventListener('paste', function(e) { 52 | inlineattach.onPaste(e); 53 | }, false); 54 | 55 | codeMirror.setOption('onDragEvent', function(data, e) { 56 | if (e.type === "drop") { 57 | e.stopPropagation(); 58 | e.preventDefault(); 59 | return inlineattach.onDrop(e); 60 | } 61 | }); 62 | }; 63 | 64 | inlineAttachment.editors.codemirror3 = codeMirrorEditor; 65 | 66 | var codeMirrorEditor4 = function(instance) { 67 | codeMirrorEditor.call(this, instance); 68 | }; 69 | 70 | codeMirrorEditor4.attach = function(codeMirror, options) { 71 | 72 | options = options || {}; 73 | 74 | var editor = new codeMirrorEditor(codeMirror), 75 | inlineattach = new inlineAttachment(options, editor), 76 | el = codeMirror.getWrapperElement(); 77 | 78 | el.addEventListener('paste', function(e) { 79 | inlineattach.onPaste(e); 80 | }, false); 81 | 82 | codeMirror.on('drop', function(data, e) { 83 | if (inlineattach.onDrop(e)) { 84 | e.stopPropagation(); 85 | e.preventDefault(); 86 | return true; 87 | } else { 88 | return false; 89 | } 90 | }); 91 | 92 | return inlineattach; 93 | }; 94 | 95 | inlineAttachment.editors.codemirror4 = codeMirrorEditor4; 96 | 97 | })(); -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_2e83ff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-icons_2e83ff_256x240.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_888888_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-icons_888888_256x240.png -------------------------------------------------------------------------------- /public/vendor/jquery-ui/images/ui-icons_cd0a0a_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/vendor/jquery-ui/images/ui-icons_cd0a0a_256x240.png -------------------------------------------------------------------------------- /public/vendor/ot/compress.sh: -------------------------------------------------------------------------------- 1 | uglifyjs --compress --mangle --output ot.min.js \ 2 | ./text-operation.js \ 3 | ./selection.js \ 4 | ./wrapped-operation.js \ 5 | ./undo-manager.js \ 6 | ./client.js \ 7 | ./codemirror-adapter.js \ 8 | ./socketio-adapter.js \ 9 | ./ajax-adapter.js \ 10 | ./editor-client.js -------------------------------------------------------------------------------- /public/vendor/ot/socketio-adapter.js: -------------------------------------------------------------------------------- 1 | /*global ot */ 2 | 3 | ot.SocketIOAdapter = (function () { 4 | 'use strict'; 5 | 6 | function SocketIOAdapter(socket) { 7 | this.socket = socket; 8 | 9 | var self = this; 10 | socket.on('client_left', function (clientId) { 11 | self.trigger('client_left', clientId); 12 | }); 13 | socket.on('set_name', function (clientId, name) { 14 | self.trigger('set_name', clientId, name); 15 | }); 16 | socket.on('set_color', function (clientId, color) { 17 | self.trigger('set_color', clientId, color); 18 | }); 19 | socket.on('ack', function (revision) { 20 | self.trigger('ack', revision); 21 | }); 22 | socket.on('operation', function (clientId, revision, operation, selection) { 23 | self.trigger('operation', revision, operation); 24 | self.trigger('selection', clientId, selection); 25 | }); 26 | socket.on('operations', function (head, operations) { 27 | self.trigger('operations', head, operations); 28 | }); 29 | socket.on('selection', function (clientId, selection) { 30 | self.trigger('selection', clientId, selection); 31 | }); 32 | socket.on('reconnect', function () { 33 | self.trigger('reconnect'); 34 | }); 35 | } 36 | 37 | SocketIOAdapter.prototype.sendOperation = function (revision, operation, selection) { 38 | this.socket.emit('operation', revision, operation, selection); 39 | }; 40 | 41 | SocketIOAdapter.prototype.sendSelection = function (selection) { 42 | this.socket.emit('selection', selection); 43 | }; 44 | 45 | SocketIOAdapter.prototype.getOperations = function (base, head) { 46 | this.socket.emit('get_operations', base, head); 47 | }; 48 | 49 | SocketIOAdapter.prototype.registerCallbacks = function (cb) { 50 | this.callbacks = cb; 51 | }; 52 | 53 | SocketIOAdapter.prototype.trigger = function (event) { 54 | var args = Array.prototype.slice.call(arguments, 1); 55 | var action = this.callbacks && this.callbacks[event]; 56 | if (action) { 57 | action.apply(this, args); 58 | } 59 | }; 60 | 61 | return SocketIOAdapter; 62 | 63 | }()); -------------------------------------------------------------------------------- /public/vendor/ot/wrapped-operation.js: -------------------------------------------------------------------------------- 1 | if (typeof ot === 'undefined') { 2 | // Export for browsers 3 | var ot = {}; 4 | } 5 | 6 | ot.WrappedOperation = (function (global) { 7 | 'use strict'; 8 | 9 | // A WrappedOperation contains an operation and corresponing metadata. 10 | function WrappedOperation (operation, meta) { 11 | this.wrapped = operation; 12 | this.meta = meta; 13 | } 14 | 15 | WrappedOperation.prototype.apply = function () { 16 | return this.wrapped.apply.apply(this.wrapped, arguments); 17 | }; 18 | 19 | WrappedOperation.prototype.invert = function () { 20 | var meta = this.meta; 21 | return new WrappedOperation( 22 | this.wrapped.invert.apply(this.wrapped, arguments), 23 | meta && typeof meta === 'object' && typeof meta.invert === 'function' ? 24 | meta.invert.apply(meta, arguments) : meta 25 | ); 26 | }; 27 | 28 | // Copy all properties from source to target. 29 | function copy (source, target) { 30 | for (var key in source) { 31 | if (source.hasOwnProperty(key)) { 32 | target[key] = source[key]; 33 | } 34 | } 35 | } 36 | 37 | function composeMeta (a, b) { 38 | if (a && typeof a === 'object') { 39 | if (typeof a.compose === 'function') { return a.compose(b); } 40 | var meta = {}; 41 | copy(a, meta); 42 | copy(b, meta); 43 | return meta; 44 | } 45 | return b; 46 | } 47 | 48 | WrappedOperation.prototype.compose = function (other) { 49 | return new WrappedOperation( 50 | this.wrapped.compose(other.wrapped), 51 | composeMeta(this.meta, other.meta) 52 | ); 53 | }; 54 | 55 | function transformMeta (meta, operation) { 56 | if (meta && typeof meta === 'object') { 57 | if (typeof meta.transform === 'function') { 58 | return meta.transform(operation); 59 | } 60 | } 61 | return meta; 62 | } 63 | 64 | WrappedOperation.transform = function (a, b) { 65 | var transform = a.wrapped.constructor.transform; 66 | var pair = transform(a.wrapped, b.wrapped); 67 | return [ 68 | new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)), 69 | new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped)) 70 | ]; 71 | }; 72 | 73 | return WrappedOperation; 74 | 75 | }(this)); 76 | 77 | // Export for CommonJS 78 | if (typeof module === 'object') { 79 | module.exports = ot.WrappedOperation; 80 | } -------------------------------------------------------------------------------- /public/vendor/showup/showup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Showup.js jQuery Plugin 3 | * http://github.com/jonschlinkert/showup 4 | * 5 | * Copyright (c) 2013 Jon Schlinkert, contributors 6 | * Licensed under the MIT License (MIT). 7 | */ 8 | 9 | 10 | (function( $ ) { 11 | $.fn.showUp = function(ele, options) { 12 | options = options || {}; 13 | 14 | var target = $(ele); 15 | var down = options.down || 'navbar-hide'; 16 | var up = options.up || 'navbar-show'; 17 | var btnHideShow = options.btnHideShow || '.btn-hide-show'; 18 | var hideOffset = options.offset || 60; 19 | var previousScroll = 0; 20 | var isHide = false; 21 | 22 | $(window).scroll(function () { 23 | checkScrollTop(); 24 | }); 25 | 26 | $(window).resize(function () { 27 | checkScrollTop(); 28 | }); 29 | 30 | $(window).mousewheel(function () { 31 | checkScrollTop(); 32 | }); 33 | 34 | function checkScrollTop() 35 | { 36 | target.clearQueue(); 37 | target.stop(); 38 | var currentScroll = $(this).scrollTop(); 39 | if (currentScroll > hideOffset && !target.hasClass('locked')) { 40 | if(Math.abs(previousScroll - currentScroll) < 50) return; 41 | if (currentScroll > previousScroll) { 42 | // Action on scroll down 43 | target.removeClass(up).addClass(down); 44 | } else if (currentScroll < previousScroll) { 45 | // Action on scroll up 46 | target.removeClass(down).addClass(up); 47 | } 48 | } else { 49 | target.removeClass(down).addClass(up); 50 | } 51 | previousScroll = $(this).scrollTop(); 52 | } 53 | 54 | // Toggle visibility of target on click 55 | $(btnHideShow).click(function () { 56 | if (target.hasClass(down)) { 57 | target.removeClass(down).addClass(up); 58 | } else { 59 | target.removeClass(up).addClass(down); 60 | } 61 | }); 62 | }; 63 | })( jQuery ); 64 | 65 | // TODO: make customizable 66 | $(document).ready(function () { 67 | var duration = 420; 68 | var showOffset = 220; 69 | var btnFixed = '.btn-fixed-bottom'; 70 | var btnToTopClass = '.back-to-top'; 71 | 72 | $(window).scroll(function () { 73 | if ($(this).scrollTop() > showOffset) { 74 | $(btnFixed).fadeIn(duration); 75 | } else { 76 | $(btnFixed).fadeOut(duration); 77 | } 78 | }); 79 | 80 | $(btnToTopClass).click(function (event) { 81 | event.preventDefault(); 82 | $('html, body').animate({ 83 | scrollTop: 0 84 | }, duration); 85 | return false; 86 | }); 87 | }); -------------------------------------------------------------------------------- /public/views/codimd.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include codimd/head %> 6 | 7 | 8 | 9 | <%- include codimd/header %> 10 | <%- include codimd/body %> 11 | <%- include codimd/footer %> 12 | <%- include codimd/foot %> 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/views/codimd/foot.ejs: -------------------------------------------------------------------------------- 1 | <% if(useCDN) { %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <%- include ../build/index-scripts %> 15 | <% } else { %> 16 | <%- include ../build/index-pack-scripts %> 17 | <% } %> 18 | -------------------------------------------------------------------------------- /public/views/codimd/footer.ejs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/views/codimd/footer.ejs -------------------------------------------------------------------------------- /public/views/codimd/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | <% if(useCDN) { %> 11 | 12 | 13 | 14 | 15 | 16 | <%- include ../build/index-header %> 17 | <%- include ../shared/polyfill %> 18 | <% } else { %> 19 | 20 | 21 | 22 | <%- include ../build/index-pack-header %> 23 | <% } %> 24 | 25 | -------------------------------------------------------------------------------- /public/views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include codimd/head %> 6 | 7 | 8 | 9 | 10 | <%- include codimd/header %> 11 |
12 |
13 |

<%- code %> <%- detail %> <%- msg %>

14 |
15 |
16 | <%- include codimd/footer %> 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/views/includes/header.ejs: -------------------------------------------------------------------------------- 1 | <% for (var css in htmlWebpackPlugin.files.css) { %> 2 | 3 | <% } %> 4 | -------------------------------------------------------------------------------- /public/views/includes/scripts.ejs: -------------------------------------------------------------------------------- 1 | 2 | <% for (var js in htmlWebpackPlugin.files.js) { %> 3 | 4 | <% } %> 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%- include index/head %> 6 | 7 | 8 | 9 | <%- include index/header %> 10 | <%- include index/body %> 11 | <%- include index/footer %> 12 | <%- include index/foot %> 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/views/index/foot.ejs: -------------------------------------------------------------------------------- 1 | <% if(useCDN) { %> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%- include ../build/cover-scripts %> 11 | <% } else { %> 12 | <%- include ../build/cover-pack-scripts %> 13 | <% } %> 14 | -------------------------------------------------------------------------------- /public/views/index/footer.ejs: -------------------------------------------------------------------------------- 1 | LiaScript 2 | -------------------------------------------------------------------------------- /public/views/index/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | CodiLIA - <%= __('Collaborative markdown notes') %> 10 | 11 | 12 | <% if(useCDN) { %> 13 | 14 | 15 | 16 | 17 | 18 | <%- include ../build/cover-header %> 19 | <%- include ../shared/polyfill %> 20 | <% } else { %> 21 | 22 | 23 | <%- include ../build/cover-pack-header %> 24 | <% } %> 25 | -------------------------------------------------------------------------------- /public/views/index/header.ejs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiaScript/CodiLIA/8374069afa2d37078bdaa1ba4889969ec803d08b/public/views/index/header.ejs -------------------------------------------------------------------------------- /public/views/shared/disqus.ejs: -------------------------------------------------------------------------------- 1 |
2 | 13 | 14 | -------------------------------------------------------------------------------- /public/views/shared/ga.ejs: -------------------------------------------------------------------------------- 1 | <% if(typeof GA !== 'undefined' && GA) { %> 2 | 18 | <% } %> 19 | -------------------------------------------------------------------------------- /public/views/shared/pandoc-export-modal.ejs: -------------------------------------------------------------------------------- 1 | 2 | 33 | -------------------------------------------------------------------------------- /public/views/shared/polyfill.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /public/views/shared/refresh-modal.ejs: -------------------------------------------------------------------------------- 1 | 2 | 32 | -------------------------------------------------------------------------------- /public/views/shared/revision-modal.ejs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/auth/oauth2/strategy.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const chance = require('chance')() 6 | 7 | const { extractProfileAttribute } = require('../../../lib/auth/oauth2/strategy') 8 | 9 | describe('OAuth2CustomStrategy', function () { 10 | describe('#extractProfileAttribute', function () { 11 | const data = { 12 | user: { 13 | email: chance.email() 14 | }, 15 | arrayData: [ 16 | { 17 | email: chance.email() 18 | }, 19 | { 20 | email: chance.email() 21 | } 22 | ] 23 | } 24 | 25 | it('should parse normal attribute correctly', function () { 26 | assert(extractProfileAttribute(data, 'user.email') === data.user.email) 27 | }) 28 | 29 | it('should return undefined when nested object key not exists', function () { 30 | assert(extractProfileAttribute(data, 'user.profile') === undefined) 31 | }) 32 | 33 | it('should return undefined when whole object key not exists', function () { 34 | assert(extractProfileAttribute(data, 'profile.email') === undefined) 35 | }) 36 | 37 | it('should return attribute in array correct', function () { 38 | assert(extractProfileAttribute(data, 'arrayData[0].email') === data.arrayData[0].email) 39 | assert(extractProfileAttribute(data, 'arrayData[1].email') === data.arrayData[1].email) 40 | }) 41 | 42 | it('should return undefined when array index out of bound', function () { 43 | assert(extractProfileAttribute(data, 'arrayData[3].email') === undefined) 44 | }) 45 | 46 | it('should return undefined when array key not exists', function () { 47 | assert(extractProfileAttribute(data, 'notExistsArray[5].email') === undefined) 48 | }) 49 | 50 | it('should return undefined when data is undefined', function () { 51 | assert(extractProfileAttribute(undefined, 'email') === undefined) 52 | assert(extractProfileAttribute(null, 'email') === undefined) 53 | assert(extractProfileAttribute({}, 'email') === undefined) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /test/letter-avatars.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 3 | 'use strict' 4 | 5 | const assert = require('assert') 6 | const mock = require('mock-require') 7 | 8 | describe('generateAvatarURL() gravatar enabled', function () { 9 | let avatars 10 | beforeEach(function () { 11 | // Reset config to make sure we don't influence other tests 12 | const testconfig = { 13 | allowGravatar: true, 14 | serverURL: 'http://localhost:3000', 15 | port: 3000 16 | } 17 | mock('../lib/config', testconfig) 18 | avatars = mock.reRequire('../lib/letter-avatars') 19 | }) 20 | 21 | it('should return correct urls', function () { 22 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', true), 'https://www.gravatar.com/avatar/d41b5f3508cc3f31865566a47dd0336b?s=400') 23 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', false), 'https://www.gravatar.com/avatar/d41b5f3508cc3f31865566a47dd0336b?s=96') 24 | }) 25 | 26 | it('should return correct urls for names with spaces', function () { 27 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels'), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') 28 | }) 29 | }) 30 | 31 | describe('generateAvatarURL() gravatar disabled', function () { 32 | let avatars 33 | beforeEach(function () { 34 | // Reset config to make sure we don't influence other tests 35 | const testconfig = { 36 | allowGravatar: false, 37 | serverURL: 'http://localhost:3000', 38 | port: 3000 39 | } 40 | mock('../lib/config', testconfig) 41 | avatars = mock.reRequire('../lib/letter-avatars') 42 | }) 43 | 44 | it('should return correct urls', function () { 45 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', true), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') 46 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', false), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') 47 | }) 48 | 49 | it('should return correct urls for names with spaces', function () { 50 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels'), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg') 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /test/realtime/cleanDanglingUser.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const mock = require('mock-require') 6 | const sinon = require('sinon') 7 | const { removeModuleFromRequireCache, makeMockSocket } = require('./utils') 8 | 9 | describe('cleanDanglingUser', function () { 10 | let clock 11 | beforeEach(() => { 12 | clock = sinon.useFakeTimers() 13 | mock('../../lib/processQueue', require('../testDoubles/ProcessQueueFake')) 14 | mock('../../lib/logger', { 15 | error: () => {}, 16 | info: () => {} 17 | }) 18 | mock('../../lib/history', {}) 19 | mock('../../lib/models', { 20 | Revision: { 21 | saveAllNotesRevision: () => { 22 | } 23 | } 24 | }) 25 | mock('../../lib/config', { 26 | debug: true 27 | }) 28 | mock('../../lib/realtimeUpdateDirtyNoteJob', require('../testDoubles/realtimeJobStub')) 29 | mock('../../lib/realtimeSaveRevisionJob', require('../testDoubles/realtimeJobStub')) 30 | }) 31 | 32 | afterEach(() => { 33 | clock.restore() 34 | removeModuleFromRequireCache('../../lib/realtime/realtime') 35 | mock.stopAll() 36 | sinon.restore() 37 | }) 38 | 39 | it('should call queueForDisconnectSpy when user is dangling', (done) => { 40 | const realtime = require('../../lib/realtime/realtime') 41 | const queueForDisconnectSpy = sinon.spy(realtime, 'queueForDisconnect') 42 | realtime.io = { 43 | to: sinon.stub().callsFake(function () { 44 | return { 45 | emit: sinon.fake() 46 | } 47 | }), 48 | sockets: { 49 | connected: {} 50 | } 51 | } 52 | const user1Socket = makeMockSocket() 53 | const user2Socket = makeMockSocket() 54 | 55 | user1Socket.rooms.push('room1') 56 | 57 | realtime.io.sockets.connected[user1Socket.id] = user1Socket 58 | realtime.io.sockets.connected[user2Socket.id] = user2Socket 59 | 60 | realtime.users[user1Socket.id] = user1Socket 61 | realtime.users[user2Socket.id] = user2Socket 62 | clock.tick(60000) 63 | clock.restore() 64 | setTimeout(() => { 65 | assert(queueForDisconnectSpy.called) 66 | done() 67 | }, 50) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /test/realtime/disconnect-process.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const mock = require('mock-require') 6 | const sinon = require('sinon') 7 | 8 | const { makeMockSocket, removeModuleFromRequireCache } = require('./utils') 9 | 10 | describe('realtime#disconnect', function () { 11 | const noteId = 'note1_id' 12 | let realtime 13 | let updateNoteStub 14 | let emitOnlineUsersStub 15 | let client 16 | 17 | beforeEach(() => { 18 | mock('../../lib/logger', { 19 | error: () => { 20 | } 21 | }) 22 | mock('../../lib/history', {}) 23 | mock('../../lib/models', { 24 | Revision: { 25 | saveAllNotesRevision: () => { 26 | } 27 | } 28 | }) 29 | mock('../../lib/config', {}) 30 | 31 | realtime = require('../../lib/realtime/realtime') 32 | updateNoteStub = sinon.stub(realtime, 'updateNote').callsFake((note, callback) => { 33 | callback(null, note) 34 | }) 35 | emitOnlineUsersStub = sinon.stub(realtime, 'emitOnlineUsers') 36 | client = makeMockSocket() 37 | client.noteId = noteId 38 | 39 | realtime.users[client.id] = { 40 | id: client.id, 41 | color: '#ff0000', 42 | cursor: null, 43 | login: false, 44 | userid: null, 45 | name: null, 46 | idle: false, 47 | type: null 48 | } 49 | 50 | realtime.getNotePool()[noteId] = { 51 | id: noteId, 52 | server: { 53 | isDirty: true 54 | }, 55 | users: { 56 | [client.id]: realtime.users[client.id] 57 | }, 58 | socks: [client] 59 | } 60 | }) 61 | 62 | afterEach(() => { 63 | removeModuleFromRequireCache('../../lib/realtime/realtime') 64 | mock.stopAll() 65 | sinon.restore() 66 | }) 67 | 68 | it('should disconnect success', function (done) { 69 | realtime.queueForDisconnect(client) 70 | 71 | setTimeout(() => { 72 | assert(typeof realtime.users[client.id] === 'undefined') 73 | assert(emitOnlineUsersStub.called) 74 | assert(updateNoteStub.called) 75 | assert(Object.keys(realtime.users).length === 0) 76 | assert(Object.keys(realtime.notes).length === 0) 77 | done() 78 | }, 5) 79 | }) 80 | 81 | it('should disconnect success when note is not dirty', function (done) { 82 | realtime.notes[noteId].server.isDirty = false 83 | realtime.queueForDisconnect(client) 84 | 85 | setTimeout(() => { 86 | assert(typeof realtime.users[client.id] === 'undefined') 87 | assert(emitOnlineUsersStub.called) 88 | assert(updateNoteStub.called === false) 89 | assert(Object.keys(realtime.users).length === 0) 90 | assert(Object.keys(realtime.notes).length === 0) 91 | done() 92 | }, 5) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /test/realtime/realtime.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* eslint-env node, mocha */ 4 | 5 | const mock = require('mock-require') 6 | const assert = require('assert') 7 | 8 | describe('realtime', function () { 9 | describe('checkViewPermission', function () { 10 | // role -> guest, loggedInUser, loggedInOwner 11 | const viewPermission = { 12 | freely: [true, true, true], 13 | editable: [true, true, true], 14 | limited: [false, true, true], 15 | locked: [true, true, true], 16 | protected: [false, true, true], 17 | private: [false, false, true] 18 | } 19 | const loggedInUserId = 'user1_id' 20 | const ownerUserId = 'user2_id' 21 | const guestReq = {} 22 | const loggedInUserReq = { 23 | user: { 24 | id: loggedInUserId, 25 | logged_in: true 26 | } 27 | } 28 | const loggedInOwnerReq = { 29 | user: { 30 | id: ownerUserId, 31 | logged_in: true 32 | } 33 | } 34 | 35 | const note = { 36 | owner: ownerUserId 37 | } 38 | 39 | let realtime 40 | 41 | beforeEach(() => { 42 | mock('../../lib/logger', { 43 | error: () => { 44 | } 45 | }) 46 | mock('../../lib/history', {}) 47 | mock('../../lib/models', { 48 | Note: { 49 | parseNoteTitle: (data) => (data) 50 | } 51 | }) 52 | mock('../../lib/config', {}) 53 | realtime = require('../../lib/realtime/realtime') 54 | }) 55 | 56 | Object.keys(viewPermission).forEach(function (permission) { 57 | describe(permission, function () { 58 | beforeEach(() => { 59 | note.permission = permission 60 | }) 61 | it('guest permission test', function () { 62 | assert(realtime.checkViewPermission(guestReq, note) === viewPermission[permission][0]) 63 | }) 64 | it('loggedIn User permission test', function () { 65 | assert(realtime.checkViewPermission(loggedInUserReq, note) === viewPermission[permission][1]) 66 | }) 67 | it('loggedIn Owner permission test', function () { 68 | assert(realtime.checkViewPermission(loggedInOwnerReq, note) === viewPermission[permission][2]) 69 | }) 70 | }) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /test/realtime/saveRevisionJob.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | 'use strict' 3 | 4 | const assert = require('assert') 5 | const mock = require('mock-require') 6 | const sinon = require('sinon') 7 | const { removeModuleFromRequireCache, removeLibModuleCache } = require('./utils') 8 | 9 | describe('save revision job', function () { 10 | let clock 11 | let mockModels 12 | let realtime 13 | beforeEach(() => { 14 | removeLibModuleCache() 15 | mockModels = { 16 | Revision: { 17 | saveAllNotesRevision: sinon.stub() 18 | } 19 | } 20 | clock = sinon.useFakeTimers() 21 | mock('../../lib/processQueue', require('../testDoubles/ProcessQueueFake')) 22 | mock('../../lib/logger', { 23 | error: () => {}, 24 | info: () => {} 25 | }) 26 | mock('../../lib/history', {}) 27 | mock('../../lib/models', mockModels) 28 | mock('../../lib/config', { 29 | debug: true 30 | }) 31 | mock('../../lib/realtimeUpdateDirtyNoteJob', require('../testDoubles/realtimeJobStub')) 32 | mock('../../lib/realtimeCleanDanglingUserJob', require('../testDoubles/realtimeJobStub')) 33 | }) 34 | 35 | afterEach(() => { 36 | clock.restore() 37 | removeModuleFromRequireCache('../../lib/realtime/realtime') 38 | removeModuleFromRequireCache('../../lib/realtime/realtimeSaveRevisionJob') 39 | mock.stopAll() 40 | sinon.restore() 41 | }) 42 | 43 | it('should execute save revision job every 5 min', (done) => { 44 | mockModels.Revision.saveAllNotesRevision.callsFake((callback) => { 45 | callback(null, []) 46 | }) 47 | realtime = require('../../lib/realtime/realtime') 48 | clock.tick(5 * 60 * 1000) 49 | clock.restore() 50 | setTimeout(() => { 51 | assert(mockModels.Revision.saveAllNotesRevision.called) 52 | assert(realtime.saveRevisionJob.getSaverSleep() === true) 53 | done() 54 | }, 50) 55 | }) 56 | 57 | it('should not set saverSleep when more than 1 note save revision', (done) => { 58 | mockModels.Revision.saveAllNotesRevision.callsFake((callback) => { 59 | callback(null, [1]) 60 | }) 61 | realtime = require('../../lib/realtime/realtime') 62 | clock.tick(5 * 60 * 1000) 63 | clock.restore() 64 | setTimeout(() => { 65 | assert(mockModels.Revision.saveAllNotesRevision.called) 66 | assert(realtime.saveRevisionJob.getSaverSleep() === false) 67 | done() 68 | }, 50) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /test/realtime/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | const path = require('path') 5 | 6 | function makeMockSocket (headers, query) { 7 | const broadCastChannelCache = {} 8 | return { 9 | id: Math.round(Math.random() * 10000), 10 | request: { 11 | user: {} 12 | }, 13 | handshake: { 14 | headers: Object.assign({}, headers), 15 | query: Object.assign({}, query) 16 | }, 17 | on: sinon.fake(), 18 | emit: sinon.fake(), 19 | broadCastChannelCache: {}, 20 | broadcast: { 21 | to: (channel) => { 22 | if (!broadCastChannelCache[channel]) { 23 | broadCastChannelCache[channel] = { 24 | channel: channel, 25 | emit: sinon.fake() 26 | } 27 | } 28 | return broadCastChannelCache[channel] 29 | } 30 | }, 31 | disconnect: sinon.fake(), 32 | rooms: [] 33 | } 34 | } 35 | 36 | function removeModuleFromRequireCache (modulePath) { 37 | delete require.cache[require.resolve(modulePath)] 38 | } 39 | function removeLibModuleCache () { 40 | const libPath = path.resolve(path.join(__dirname, '../../lib')) 41 | Object.keys(require.cache).forEach(key => { 42 | if (key.startsWith(libPath)) { 43 | delete require.cache[require.resolve(key)] 44 | } 45 | }) 46 | } 47 | 48 | exports.makeMockSocket = makeMockSocket 49 | exports.removeModuleFromRequireCache = removeModuleFromRequireCache 50 | exports.removeLibModuleCache = removeLibModuleCache 51 | -------------------------------------------------------------------------------- /test/testDoubles/ProcessQueueFake.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class ProcessQueueFake { 4 | constructor () { 5 | this.taskMap = new Map() 6 | this.queue = [] 7 | } 8 | 9 | start () { 10 | 11 | } 12 | 13 | stop () { 14 | 15 | } 16 | 17 | checkTaskIsInQueue (id) { 18 | return this.taskMap.has(id) 19 | } 20 | 21 | push (id, processFunc) { 22 | this.queue.push({ 23 | id: id, 24 | processFunc: processFunc 25 | }) 26 | this.taskMap.set(id, true) 27 | } 28 | 29 | process () { 30 | 31 | } 32 | } 33 | 34 | exports.ProcessQueueFake = ProcessQueueFake 35 | exports.ProcessQueue = ProcessQueueFake 36 | -------------------------------------------------------------------------------- /test/testDoubles/loggerFake.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | 5 | function createFakeLogger () { 6 | return { 7 | error: sinon.stub(), 8 | warn: sinon.stub(), 9 | info: sinon.stub(), 10 | debug: sinon.stub(), 11 | log: sinon.stub() 12 | } 13 | } 14 | 15 | exports.createFakeLogger = createFakeLogger 16 | -------------------------------------------------------------------------------- /test/testDoubles/otFake.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const sinon = require('sinon') 4 | 5 | class EditorSocketIOServerFake { 6 | constructor () { 7 | this.addClient = sinon.stub() 8 | this.onOperation = sinon.stub() 9 | this.onGetOperations = sinon.stub() 10 | this.updateSelection = sinon.stub() 11 | this.setName = sinon.stub() 12 | this.setColor = sinon.stub() 13 | this.getClient = sinon.stub() 14 | this.onDisconnect = sinon.stub() 15 | } 16 | } 17 | 18 | exports.EditorSocketIOServer = EditorSocketIOServerFake 19 | -------------------------------------------------------------------------------- /test/testDoubles/realtimeJobStub.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class realtimeJobStub { 4 | start () { 5 | } 6 | 7 | stop () { 8 | } 9 | } 10 | 11 | exports.realtimeJobStub = realtimeJobStub 12 | exports.UpdateDirtyNoteJob = realtimeJobStub 13 | exports.CleanDanglingUserJob = realtimeJobStub 14 | exports.SaveRevisionJob = realtimeJobStub 15 | -------------------------------------------------------------------------------- /utils/string.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function stripTags (s) { 4 | return s.replace(RegExp(`]*>`, 'gi'), '') 5 | } 6 | 7 | exports.stripTags = stripTags 8 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.js') 2 | const htmlexport = require('./webpack.htmlexport') 3 | const merge = require('webpack-merge') 4 | 5 | module.exports = [ 6 | // merge common config 7 | merge(common, { 8 | mode: 'development', 9 | devtool: 'cheap-module-eval-source-map' 10 | }), 11 | merge(htmlexport, { 12 | mode: 'development', 13 | devtool: 'cheap-module-eval-source-map' 14 | })] 15 | -------------------------------------------------------------------------------- /webpack.htmlexport.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | name: 'save-as-html', 6 | entry: { 7 | htmlExport: path.join(__dirname, 'public/js/htmlExport.js') 8 | }, 9 | module: { 10 | rules: [{ 11 | test: /\.css$/, 12 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 13 | }] 14 | }, 15 | output: { 16 | path: path.join(__dirname, 'public/build'), 17 | publicPath: '/build/', 18 | filename: '[name].js' 19 | }, 20 | plugins: [ 21 | new MiniCssExtractPlugin({ 22 | filename: 'html.min.css' 23 | }) 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const common = require('./webpack.common.js') 2 | const htmlexport = require('./webpack.htmlexport') 3 | const merge = require('webpack-merge') 4 | const path = require('path') 5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 6 | 7 | module.exports = [ 8 | merge(common, { 9 | mode: 'production', 10 | output: { 11 | path: path.join(__dirname, 'public/build'), 12 | publicPath: '/build/', 13 | filename: '[name].[contenthash].js' 14 | } 15 | }), 16 | merge(htmlexport, { 17 | mode: 'production', 18 | optimization: { 19 | minimizer: [ 20 | new OptimizeCSSAssetsPlugin({}) 21 | ] 22 | } 23 | })] 24 | --------------------------------------------------------------------------------