|
@ -1,5 +1,4 @@
|
|||
node_modules
|
||||
package-lock.json
|
||||
composer.phar
|
||||
composer.lock
|
||||
.env.*.php
|
||||
|
|
|
@ -7,7 +7,7 @@ node_js:
|
|||
- "12"
|
||||
|
||||
dist: xenial
|
||||
cache: yarn
|
||||
cache: npm
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
@ -19,8 +19,8 @@ matrix:
|
|||
- node_js: "12"
|
||||
|
||||
script:
|
||||
- yarn test:ci
|
||||
- yarn build
|
||||
- npm run test:ci
|
||||
- npm run build
|
||||
|
||||
jobs:
|
||||
include:
|
||||
|
|
|
@ -44,7 +44,7 @@ HackMD team is committed to keep CodiMD open source. All contributions are welco
|
|||
You would find all documentation here: [CodiMD Documentation](https://hackmd.io/c/codimd-documentation)
|
||||
|
||||
### Deployment
|
||||
If you want to spin up an instance and start using immediately, see [Docker deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-documentation#Deployment).
|
||||
If you want to spin up an instance and start using immediately, see [Docker deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-docker-deployment).
|
||||
If you want to contribute to the project, start with [manual deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-manual-deployment).
|
||||
|
||||
### Configuration
|
||||
|
|
33
app.js
|
@ -24,6 +24,9 @@ var logger = require('./lib/logger')
|
|||
var response = require('./lib/response')
|
||||
var models = require('./lib/models')
|
||||
var csp = require('./lib/csp')
|
||||
const { Environment } = require('./lib/config/enum')
|
||||
|
||||
const { versionCheckMiddleware, checkVersion } = require('./lib/web/middleware/checkVersion')
|
||||
|
||||
function createHttpServer () {
|
||||
if (config.useSSL) {
|
||||
|
@ -66,7 +69,7 @@ io.engine.ws = new (require('ws').Server)({
|
|||
})
|
||||
|
||||
// others
|
||||
var realtime = require('./lib/realtime.js')
|
||||
var realtime = require('./lib/realtime/realtime.js')
|
||||
|
||||
// assign socket io to realtime
|
||||
realtime.io = io
|
||||
|
@ -153,7 +156,7 @@ server.on('resumeSession', function (id, cb) {
|
|||
})
|
||||
|
||||
// middleware which blocks requests when we're too busy
|
||||
app.use(require('./lib/web/middleware/tooBusy'))
|
||||
app.use(require('./lib/middleware/tooBusy'))
|
||||
|
||||
app.use(flash())
|
||||
|
||||
|
@ -162,10 +165,15 @@ app.use(passport.initialize())
|
|||
app.use(passport.session())
|
||||
|
||||
// check uri is valid before going further
|
||||
app.use(require('./lib/web/middleware/checkURIValid'))
|
||||
app.use(require('./lib/middleware/checkURIValid'))
|
||||
// redirect url without trailing slashes
|
||||
app.use(require('./lib/web/middleware/redirectWithoutTrailingSlashes'))
|
||||
app.use(require('./lib/web/middleware/codiMDVersion'))
|
||||
app.use(require('./lib/middleware/redirectWithoutTrailingSlashes'))
|
||||
app.use(require('./lib/middleware/codiMDVersion'))
|
||||
|
||||
if (config.autoVersionCheck && process.env.NODE_ENV === Environment.production) {
|
||||
checkVersion(app)
|
||||
app.use(versionCheckMiddleware)
|
||||
}
|
||||
|
||||
// routes need sessions
|
||||
// template files
|
||||
|
@ -186,6 +194,7 @@ app.locals.authProviders = {
|
|||
facebook: config.isFacebookEnable,
|
||||
twitter: config.isTwitterEnable,
|
||||
github: config.isGitHubEnable,
|
||||
bitbucket: config.isBitbucketEnable,
|
||||
gitlab: config.isGitLabEnable,
|
||||
mattermost: config.isMattermostEnable,
|
||||
dropbox: config.isDropboxEnable,
|
||||
|
@ -199,23 +208,21 @@ app.locals.authProviders = {
|
|||
email: config.isEmailEnable,
|
||||
allowEmailRegister: config.allowEmailRegister
|
||||
}
|
||||
app.locals.versionInfo = {
|
||||
latest: true,
|
||||
versionItem: null
|
||||
}
|
||||
|
||||
// Export/Import menu items
|
||||
app.locals.enableDropBoxSave = config.isDropboxEnable
|
||||
app.locals.enableGitHubGist = config.isGitHubEnable
|
||||
app.locals.enableGitlabSnippets = config.isGitlabSnippetsEnable
|
||||
|
||||
app.use(require('./lib/web/baseRouter'))
|
||||
app.use(require('./lib/web/statusRouter'))
|
||||
app.use(require('./lib/web/auth'))
|
||||
app.use(require('./lib/web/historyRouter'))
|
||||
app.use(require('./lib/web/userRouter'))
|
||||
app.use(require('./lib/web/imageRouter'))
|
||||
app.use(require('./lib/web/noteRouter'))
|
||||
app.use(require('./lib/routes').router)
|
||||
|
||||
// response not found if no any route matxches
|
||||
app.get('*', function (req, res) {
|
||||
response.errorNotFound(res)
|
||||
response.errorNotFound(req, res)
|
||||
})
|
||||
|
||||
// socket.io secure
|
||||
|
|
68
app.json
|
@ -15,124 +15,132 @@
|
|||
"description": "Let npm also install development build tool",
|
||||
"value": "false"
|
||||
},
|
||||
"HMD_SESSION_SECRET": {
|
||||
"CMD_SESSION_SECRET": {
|
||||
"description": "Secret used to secure session cookies.",
|
||||
"required": false
|
||||
},
|
||||
"HMD_HSTS_ENABLE": {
|
||||
"CMD_HSTS_ENABLE": {
|
||||
"description": "whether to also use HSTS if HTTPS is enabled",
|
||||
"required": false
|
||||
},
|
||||
"HMD_HSTS_MAX_AGE": {
|
||||
"CMD_HSTS_MAX_AGE": {
|
||||
"description": "max duration, in seconds, to tell clients to keep HSTS status",
|
||||
"required": false
|
||||
},
|
||||
"HMD_HSTS_INCLUDE_SUBDOMAINS": {
|
||||
"CMD_HSTS_INCLUDE_SUBDOMAINS": {
|
||||
"description": "whether to tell clients to also regard subdomains as HSTS hosts",
|
||||
"required": false
|
||||
},
|
||||
"HMD_HSTS_PRELOAD": {
|
||||
"CMD_HSTS_PRELOAD": {
|
||||
"description": "whether to allow at all adding of the site to HSTS preloads (e.g. in browsers)",
|
||||
"required": false
|
||||
},
|
||||
"HMD_DOMAIN": {
|
||||
"CMD_DOMAIN": {
|
||||
"description": "domain name",
|
||||
"required": false
|
||||
},
|
||||
"HMD_URL_PATH": {
|
||||
"CMD_URL_PATH": {
|
||||
"description": "sub url path, like `www.example.com/<URL_PATH>`",
|
||||
"required": false
|
||||
},
|
||||
"HMD_ALLOW_ORIGIN": {
|
||||
"CMD_ALLOW_ORIGIN": {
|
||||
"description": "domain name whitelist (use comma to separate)",
|
||||
"required": false,
|
||||
"value": "localhost"
|
||||
},
|
||||
"HMD_PROTOCOL_USESSL": {
|
||||
"CMD_PROTOCOL_USESSL": {
|
||||
"description": "set to use ssl protocol for resources path (only applied when domain is set)",
|
||||
"required": false
|
||||
},
|
||||
"HMD_URL_ADDPORT": {
|
||||
"CMD_URL_ADDPORT": {
|
||||
"description": "set to add port on callback url (port 80 or 443 won't applied) (only applied when domain is set)",
|
||||
"required": false
|
||||
},
|
||||
"HMD_FACEBOOK_CLIENTID": {
|
||||
"CMD_FACEBOOK_CLIENTID": {
|
||||
"description": "Facebook API client id",
|
||||
"required": false
|
||||
},
|
||||
"HMD_FACEBOOK_CLIENTSECRET": {
|
||||
"CMD_FACEBOOK_CLIENTSECRET": {
|
||||
"description": "Facebook API client secret",
|
||||
"required": false
|
||||
},
|
||||
"HMD_TWITTER_CONSUMERKEY": {
|
||||
"CMD_TWITTER_CONSUMERKEY": {
|
||||
"description": "Twitter API consumer key",
|
||||
"required": false
|
||||
},
|
||||
"HMD_TWITTER_CONSUMERSECRET": {
|
||||
"CMD_TWITTER_CONSUMERSECRET": {
|
||||
"description": "Twitter API consumer secret",
|
||||
"required": false
|
||||
},
|
||||
"HMD_GITHUB_CLIENTID": {
|
||||
"CMD_GITHUB_CLIENTID": {
|
||||
"description": "GitHub API client id",
|
||||
"required": false
|
||||
},
|
||||
"HMD_GITHUB_CLIENTSECRET": {
|
||||
"CMD_GITHUB_CLIENTSECRET": {
|
||||
"description": "GitHub API client secret",
|
||||
"required": false
|
||||
},
|
||||
"HMD_GITLAB_BASEURL": {
|
||||
"CMD_BITBUCKET_CLIENTID": {
|
||||
"description": "Bitbucket API client id",
|
||||
"required": false
|
||||
},
|
||||
"CMD_BITBUCKET_CLIENTSECRET": {
|
||||
"description": "Bitbucket API client secret",
|
||||
"required": false
|
||||
},
|
||||
"CMD_GITLAB_BASEURL": {
|
||||
"description": "GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional)",
|
||||
"required": false
|
||||
},
|
||||
"HMD_GITLAB_CLIENTID": {
|
||||
"CMD_GITLAB_CLIENTID": {
|
||||
"description": "GitLab API client id",
|
||||
"required": false
|
||||
},
|
||||
"HMD_GITLAB_CLIENTSECRET": {
|
||||
"CMD_GITLAB_CLIENTSECRET": {
|
||||
"description": "GitLab API client secret",
|
||||
"required": false
|
||||
},
|
||||
"HMD_GITLAB_SCOPE": {
|
||||
"CMD_GITLAB_SCOPE": {
|
||||
"description": "GitLab API client scope (optional)",
|
||||
"required": false
|
||||
},
|
||||
"HMD_MATTERMOST_BASEURL": {
|
||||
"CMD_MATTERMOST_BASEURL": {
|
||||
"description": "Mattermost authentication endpoint",
|
||||
"required": false
|
||||
},
|
||||
"HMD_MATTERMOST_CLIENTID": {
|
||||
"CMD_MATTERMOST_CLIENTID": {
|
||||
"description": "Mattermost API client id",
|
||||
"required": false
|
||||
},
|
||||
"HMD_MATTERMOST_CLIENTSECRET": {
|
||||
"CMD_MATTERMOST_CLIENTSECRET": {
|
||||
"description": "Mattermost API client secret",
|
||||
"required": false
|
||||
},
|
||||
"HMD_DROPBOX_CLIENTID": {
|
||||
"CMD_DROPBOX_CLIENTID": {
|
||||
"description": "Dropbox API client id",
|
||||
"required": false
|
||||
},
|
||||
"HMD_DROPBOX_CLIENTSECRET": {
|
||||
"CMD_DROPBOX_CLIENTSECRET": {
|
||||
"description": "Dropbox API client secret",
|
||||
"required": false
|
||||
},
|
||||
"HMD_DROPBOX_APP_KEY": {
|
||||
"CMD_DROPBOX_APP_KEY": {
|
||||
"description": "Dropbox app key (for import/export)",
|
||||
"required": false
|
||||
},
|
||||
"HMD_GOOGLE_CLIENTID": {
|
||||
"CMD_GOOGLE_CLIENTID": {
|
||||
"description": "Google API client id",
|
||||
"required": false
|
||||
},
|
||||
"HMD_GOOGLE_CLIENTSECRET": {
|
||||
"CMD_GOOGLE_CLIENTSECRET": {
|
||||
"description": "Google API client secret",
|
||||
"required": false
|
||||
},
|
||||
"HMD_IMGUR_CLIENTID": {
|
||||
"CMD_IMGUR_CLIENTID": {
|
||||
"description": "Imgur API client id",
|
||||
"required": false
|
||||
},
|
||||
"HMD_ALLOW_PDF_EXPORT": {
|
||||
"CMD_ALLOW_PDF_EXPORT": {
|
||||
"description": "Enable or disable PDF exports",
|
||||
"required": false
|
||||
}
|
||||
|
|
12
bin/heroku
|
@ -4,17 +4,7 @@ set -e
|
|||
|
||||
if [ ! -z "$DYNO" ]; then
|
||||
# setup config files
|
||||
cat << EOF > .sequelizerc
|
||||
var path = require('path');
|
||||
|
||||
module.exports = {
|
||||
'config': path.resolve('config.json'),
|
||||
'migrations-path': path.resolve('lib', 'migrations'),
|
||||
'models-path': path.resolve('lib', 'models'),
|
||||
'url': process.env.DATABASE_URL
|
||||
}
|
||||
|
||||
EOF
|
||||
cp .sequelizerc.example .sequelizerc
|
||||
|
||||
cat << EOF > config.json
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CMD_DB_URL="$DATABASE_URL" CMD_PORT="$PORT" npm run start
|
10
bin/setup
|
@ -8,12 +8,11 @@ if [ -d .git ]; then
|
|||
cd "$(git rev-parse --show-toplevel)"
|
||||
fi
|
||||
|
||||
if ! type yarn > /dev/null
|
||||
if ! type npm > /dev/null
|
||||
then
|
||||
cat << EOF
|
||||
yarn is not installed, please install Node.js, npm and yarn.
|
||||
npm is not installed, please install Node.js and npm.
|
||||
Read more on Node.js official website: https://nodejs.org
|
||||
And for yarn package manager at: https://yarnpkg.com/en/
|
||||
Setup will not be run
|
||||
EOF
|
||||
exit 0
|
||||
|
@ -29,14 +28,13 @@ if [ ! -f .sequelizerc ]; then
|
|||
fi
|
||||
|
||||
echo "install packages"
|
||||
yarn install --pure-lockfile
|
||||
yarn install --production=false --pure-lockfile
|
||||
npm install
|
||||
|
||||
cat << EOF
|
||||
|
||||
|
||||
Edit the following config file to setup CodiMD server and client.
|
||||
Read more info at https://github.com/hackmdio/codimd#configuration-files
|
||||
Read more info at https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-configuration
|
||||
|
||||
* config.json -- CodiMD config
|
||||
* .sequelizerc -- db config
|
||||
|
|
|
@ -5,14 +5,13 @@ COPY --chown=hackmd:hackmd . .
|
|||
RUN set -xe && \
|
||||
git reset --hard && \
|
||||
git clean -fx && \
|
||||
yarn install && \
|
||||
yarn build && \
|
||||
yarn install --production=true && \
|
||||
npm install && \
|
||||
npm run build && \
|
||||
cp ./deployments/docker-entrypoint.sh ./ && \
|
||||
cp .sequelizerc.example .sequelizerc && \
|
||||
rm -rf .git .gitignore .travis.yml .dockerignore .editorconfig .babelrc .mailmap .sequelizerc.example \
|
||||
test docs contribute \
|
||||
yarn.lock webpack.prod.js webpack.htmlexport.js webpack.dev.js webpack.common.js \
|
||||
package-lock.json webpack.prod.js webpack.htmlexport.js webpack.dev.js webpack.common.js \
|
||||
config.json.example README.md CONTRIBUTING.md AUTHORS
|
||||
|
||||
FROM hackmdio/runtime:1.0.6
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
version: "3"
|
||||
services:
|
||||
database:
|
||||
image: postgres:11.5
|
||||
image: postgres:11.6-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=codimd
|
||||
- POSTGRES_PASSWORD=change_password
|
||||
|
@ -11,10 +11,10 @@ services:
|
|||
restart: always
|
||||
codimd:
|
||||
# you can use image or custom build below
|
||||
# image: nabo.codimd.dev/hackmdio/hackmd:1.4.0
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ./deployments/Dockerfile
|
||||
image: nabo.codimd.dev/hackmdio/hackmd:2.0.0
|
||||
# build:
|
||||
# context: ..
|
||||
# dockerfile: ./deployments/Dockerfile
|
||||
environment:
|
||||
- CMD_DB_URL=postgres://codimd:change_password@database/codimd
|
||||
- CMD_USECDN=false
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
# Webpack Docs
|
||||
## `webpack.common.js`
|
||||
This file contains all common definition for chunks and plugins, that are needed by the whole app.
|
||||
|
||||
**TODO:** Document which entry points are used for what.
|
||||
|
||||
## `webpack.htmlexport.js`
|
||||
Separate config for the "save as html" feature.
|
||||
Packs all CSS from `public/js/htmlExport.js` to `build/html.min.css`.
|
||||
This file is then downloaded by client-side JS and used to create the HTML.
|
||||
See `exportToHTML()` in `public/js/extra.js`.
|
||||
|
||||
|
||||
## `webpack.dev.js`
|
||||
The development config uses both common configs, enables development mode and enables "cheap" source maps (lines only).
|
||||
If you need more detailed source maps while developing, you might want to use the `source-maps` option.
|
||||
See https://webpack.js.org/configuration/devtool/ for details.
|
||||
|
||||
## `webpack.prod.js`
|
||||
The production config uses both common configs and enables production mode.
|
||||
This automatically enables various optimizations (e.g. UglifyJS). See https://webpack.js.org/concepts/mode/ for details.
|
||||
|
||||
For the global app config, the name of the emitted chunks is changed to include the content hash.
|
||||
See https://webpack.js.org/guides/caching/ on why this is a good idea.
|
||||
|
||||
For the HTML export config, CSS minification is enabled.
|
|
@ -1,38 +0,0 @@
|
|||
Authentication guide - GitHub
|
||||
===
|
||||
|
||||
***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!*
|
||||
|
||||
1. Sign-in or sign-up for a GitHub account
|
||||
2. Navigate to developer settings in your GitHub account [here](https://github.com/settings/developers) and select the "OAuth Apps" tab
|
||||
3. Click on the **New OAuth App** button, to create a new OAuth App:
|
||||
|
||||
![create-oauth-app](../images/auth/create-oauth-app.png)
|
||||
|
||||
4. Fill out the new OAuth application registration form, and click **Register Application**
|
||||
|
||||
![register-oauth-application-form](../images/auth/register-oauth-application-form.png)
|
||||
|
||||
*Note: The callback URL is <your-hackmd-url>/auth/github/callback*
|
||||
|
||||
5. After successfully registering the application, you'll receive the Client ID and Client Secret for the application
|
||||
|
||||
![application-page](../images/auth/application-page.png)
|
||||
|
||||
6. Add the Client ID and Client Secret to your config.json file or pass them as environment variables
|
||||
* config.json:
|
||||
````javascript
|
||||
{
|
||||
"production": {
|
||||
"github": {
|
||||
"clientID": "3747d30eaccXXXXXXXXX",
|
||||
"clientSecret": "2a8e682948eee0c580XXXXXXXXXXXXXXXXXXXXXX"
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
* environment variables:
|
||||
````
|
||||
HMD_GITHUB_CLIENTID=3747d30eaccXXXXXXXXX
|
||||
HMD_GITHUB_CLIENTSECRET=2a8e682948eee0c580XXXXXXXXXXXXXXXXXXXXXX
|
||||
````
|
|
@ -1,32 +0,0 @@
|
|||
# GitLab (self-hosted)
|
||||
===
|
||||
|
||||
***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!*
|
||||
|
||||
1. Sign in to your GitLab
|
||||
2. Navigate to the application management page at `https://your.gitlab.domain/admin/applications` (admin permissions required)
|
||||
3. Click **New application** to create a new application and fill out the registration form:
|
||||
|
||||
![New GitLab application](../images/auth/gitlab-new-application.png)
|
||||
|
||||
4. Click **Submit**
|
||||
5. In the list of applications select **HackMD**. Leave that site open to copy the application ID and secret in the next step.
|
||||
|
||||
![Application: HackMD](../images/auth/gitlab-application-details.png)
|
||||
|
||||
|
||||
6. In the `docker-compose.yml` add the following environment variables to `app:` `environment:`
|
||||
|
||||
```
|
||||
- HMD_DOMAIN=your.hackmd.domain
|
||||
- HMD_URL_ADDPORT=443
|
||||
- HMD_PROTOCOL_USESSL=true
|
||||
- HMD_GITLAB_BASEURL=https://your.gitlab.domain
|
||||
- HMD_GITLAB_CLIENTID=23462a34example99XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
- HMD_GITLAB_CLIENTSECRET=5532e9dexamplXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
```
|
||||
|
||||
7. Run `docker-compose up -d` to apply your settings.
|
||||
8. Sign in to your HackMD using your GitLab ID:
|
||||
|
||||
![Sign in via GitLab](../images/auth/gitlab-sign-in.png)
|
|
@ -1,42 +0,0 @@
|
|||
AD LDAP auth
|
||||
===
|
||||
|
||||
|
||||
To setup your CodiMD instance with Active Directory you need the following configs:
|
||||
|
||||
```
|
||||
CMD_LDAP_URL=ldap://internal.example.com
|
||||
CMD_LDAP_BINDDN=cn=binduser,cn=Users,dc=internal,dc=example,dc=com
|
||||
CMD_LDAP_BINDCREDENTIALS=<super secret password>
|
||||
CMD_LDAP_SEARCHBASE=dc=internal,dc=example,dc=com
|
||||
CMD_LDAP_SEARCHFILTER=(&(objectcategory=person)(objectclass=user)(|(sAMAccountName={{username}})(mail={{username}})))
|
||||
CMD_LDAP_USERIDFIELD=sAMAccountName
|
||||
CMD_LDAP_PROVIDERNAME=Example Inc AD
|
||||
```
|
||||
|
||||
|
||||
`CMD_LDAP_BINDDN` is either the `distinguishedName` or the `userPrincipalName`. *This can cause "username/password is invalid" when either this value or the password from `CMD_LDAP_BINDCREDENTIALS` are incorrect.*
|
||||
|
||||
`CMD_LDAP_SEARCHFILTER` matches on all users and uses either the email address or the `sAMAccountName` (usually the login name you also use to login to Windows).
|
||||
|
||||
*Only using `sAMAccountName` looks like this:* `(&(objectcategory=person)(objectclass=user)(sAMAccountName={{username}}))`
|
||||
|
||||
`CMD_LDAP_USERIDFIELD` says we want to use `sAMAccountName` as unique identifier for the account itself.
|
||||
|
||||
`CMD_LDAP_PROVIDERNAME` just the name written above the username and password field on the login page.
|
||||
|
||||
|
||||
Same in json:
|
||||
|
||||
```json
|
||||
"ldap": {
|
||||
"url": "ldap://internal.example.com",
|
||||
"bindDn": "cn=binduser,cn=Users,dc=internal,dc=example,dc=com",
|
||||
"bindCredentials": "<super secret password>",
|
||||
"searchBase": "dc=internal,dc=example,dc=com",
|
||||
"searchFilter": "(&(objectcategory=person)(objectclass=user)(|(sAMAccountName={{username}})(mail={{username}})))",
|
||||
"useridField": "sAMAccountName",
|
||||
},
|
||||
```
|
||||
|
||||
More details and example: https://www.npmjs.com/package/passport-ldapauth
|
|
@ -1,58 +0,0 @@
|
|||
Authentication guide - Mattermost (self-hosted)
|
||||
===
|
||||
|
||||
*Note: The Mattermost setup portion of this document is just a quick guide. See the [official documentation](https://docs.mattermost.com/developer/oauth-2-0-applications.html) for more details.*
|
||||
|
||||
This guide uses the generic OAuth2 module for compatibility with Mattermost version 5.0 and above.
|
||||
|
||||
1. Sign-in with an administrator account to your Mattermost instance
|
||||
2. Make sure **OAuth 2.0 Service Provider** is enabled in the Main Menu (menu button next to your username in the top left corner) --> System Console --> Custom Integrations menu, which you can find at `https://your.mattermost.domain/admin_console/integrations/custom`
|
||||
|
||||
![mattermost-enable-oauth2](../images/auth/mattermost-enable-oauth2.png)
|
||||
|
||||
3. Navigate to the OAuth integration settings through Main Menu --> Integrations --> OAuth 2.0 Applications, at `https://your.mattermost.domain/yourteam/integrations/oauth2-apps`
|
||||
4. Click on the **Add OAuth 2.0 Application** button to add a new OAuth application
|
||||
|
||||
![mattermost-oauth-app-add](../images/auth/mattermost-oauth-app-add.png)
|
||||
|
||||
5. Fill out the form and click **Save**
|
||||
|
||||
![mattermost-oauth-app-form](../images/auth/mattermost-oauth-app-form.png)
|
||||
|
||||
*Note: The callback URL is \<your-codimd-url\>/auth/oauth2/callback*
|
||||
|
||||
6. After saving the application, you'll receive the Client ID and Client Secret
|
||||
|
||||
![mattermost-oauth-app-done](../images/auth/mattermost-oauth-app-done.png)
|
||||
|
||||
7. Add the Client ID and Client Secret to your config.json file or pass them as environment variables
|
||||
* config.json:
|
||||
````javascript
|
||||
{
|
||||
"production": {
|
||||
"oauth2": {
|
||||
"baseURL": "https://your.mattermost.domain",
|
||||
"userProfileURL": "https://your.mattermost.domain/api/v4/users/me",
|
||||
"userProfileUsernameAttr": "id",
|
||||
"userProfileDisplayNameAttr": "username",
|
||||
"userProfileEmailAttr": "email",
|
||||
"tokenURL": "https://your.mattermost.domain/oauth/access_token",
|
||||
"authorizationURL": "https://your.mattermost.domain/oauth/authorize",
|
||||
"clientID": "ii4p1u3jz7dXXXXXXXXXXXXXXX",
|
||||
"clientSecret": "mqzzx6fydbXXXXXXXXXXXXXXXX"
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
* environment variables:
|
||||
````
|
||||
CMD_OAUTH2_BASEURL=https://your.mattermost.domain
|
||||
CMD_OAUTH2_USER_PROFILE_URL=https://your.mattermost.domain/api/v4/users/me
|
||||
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=id
|
||||
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=username
|
||||
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
|
||||
CMD_OAUTH2_TOKEN_URL=https://your.mattermost.domain/oauth/access_token
|
||||
CMD_OAUTH2_AUTHORIZATION_URL=https://your.mattermost.domain/oauth/authorize
|
||||
CMD_OAUTH2_CLIENT_ID=ii4p1u3jz7dXXXXXXXXXXXXXXX
|
||||
CMD_OAUTH2_CLIENT_SECRET=mqzzx6fydbXXXXXXXXXXXXXXXX
|
||||
````
|
|
@ -1,52 +0,0 @@
|
|||
Authentication guide - Nextcloud (self-hosted)
|
||||
===
|
||||
|
||||
*This has been constructed using the [Nextcloud OAuth2 Documentation](https://docs.nextcloud.com/server/14/admin_manual/configuration_server/oauth2.html?highlight=oauth2) combined with [this issue comment on the nextcloud bugtracker](https://github.com/nextcloud/server/issues/5694#issuecomment-314761326).*
|
||||
|
||||
This guide uses the generic OAuth2 module for compatibility with Nextcloud 13 and above (this guide has been tested successfully with Nextcloud 14).
|
||||
|
||||
1. Sign-in with an administrator account to your Nextcloud server
|
||||
|
||||
2. Navigate to the OAuth integration settings: Profile Icon (top right) --> Settings
|
||||
Then choose Security Settings from the *Administration* part of the list - Don't confuse this with Personal Security Settings, where you would change your personal password!
|
||||
At the top there's OAuth 2.0-Clients.
|
||||
![Where to find OAuth2 in Nextcloud](../images/auth/nextcloud-oauth2-1-settings.png)
|
||||
|
||||
3. Add your CodiMD instance by giving it a *name* (perhaps CodiMD, but could be anything) and a *Redirection-URI*. The Redirection-URI will be `\<your-codimd-url\>/auth/oauth2/callback`. Click <kbd>Add</kbd>.
|
||||
![Adding a client to Nextcloud](../images/auth/nextcloud-oauth2-2-client-add.png)
|
||||
|
||||
|
||||
4. You'll now see a line containing a *client identifier* and a *Secret*.
|
||||
![Successfully added OAuth2-client](../images/auth/nextcloud-oauth2-3-clientid-secret.png)
|
||||
|
||||
5. That's it for Nextcloud, the rest is configured in your CodiMD `config.json` or via the `CMD_` environment variables!
|
||||
|
||||
6. Add the Client ID and Client Secret to your `config.json` file or pass them as environment variables. Make sure you also replace `<your-nextcloud-domain>` with the right domain name.
|
||||
* `config.json`:
|
||||
```javascript
|
||||
{
|
||||
"production": {
|
||||
"oauth2": {
|
||||
"clientID": "ii4p1u3jz7dXXXXXXXXXXXXXXX",
|
||||
"clientSecret": "mqzzx6fydbXXXXXXXXXXXXXXXX",
|
||||
"authorizationURL": "https://<your-nextcloud-domain>/apps/oauth2/authorize",
|
||||
"tokenURL": "https://<your-nextcloud-domain>/apps/oauth2/api/v1/token",
|
||||
"userProfileURL": "https://<your-nextcloud-domain>/ocs/v2.php/cloud/user?format=json",
|
||||
"userProfileUsernameAttr": "ocs.data.id",
|
||||
"userProfileDisplayNameAttr": "ocs.data.display-name",
|
||||
"userProfileEmailAttr": "ocs.data.email"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
* environment variables:
|
||||
```sh
|
||||
CMD_OAUTH2_CLIENT_ID=ii4p1u3jz7dXXXXXXXXXXXXXXX
|
||||
CMD_OAUTH2_CLIENT_SECRET=mqzzx6fydbXXXXXXXXXXXXXXXX
|
||||
CMD_OAUTH2_AUTHORIZATION_URL=https://<your-nextcloud-domain>/apps/oauth2/authorize
|
||||
CMD_OAUTH2_TOKEN_URL=https://<your-nextcloud-domain>/apps/oauth2/api/v1/token
|
||||
CMD_OAUTH2_USER_PROFILE_URL=https://<your-nextcloud-domain>/ocs/v2.php/cloud/user?format=json
|
||||
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=ocs.data.id
|
||||
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=ocs.data.display-name
|
||||
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=ocs.data.email
|
||||
```
|
|
@ -1,54 +0,0 @@
|
|||
Authentication guide - SAML (OneLogin)
|
||||
===
|
||||
|
||||
***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!*
|
||||
|
||||
1. Sign-in or sign-up for an OneLogin account. (available free trial for 2 weeks)
|
||||
2. Go to the administration page.
|
||||
3. Select the **APPS** menu and click on the **Add Apps**.
|
||||
|
||||
![onelogin-add-app](../images/auth/onelogin-add-app.png)
|
||||
|
||||
4. Find "SAML Test Connector (SP)" for template of settings and select it.
|
||||
|
||||
![onelogin-select-template](../images/auth/onelogin-select-template.png)
|
||||
|
||||
5. Edit display name and icons for OneLogin dashboard as you want, and click **SAVE**.
|
||||
|
||||
![onelogin-edit-app-name](../images/auth/onelogin-edit-app-name.png)
|
||||
|
||||
6. After that other tabs will appear, click the **Configuration**, and fill out the below items, and click **SAVE**.
|
||||
* RelayState: The base URL of your hackmd, which is issuer. (last slash is not needed)
|
||||
* ACS (Consumer) URL Validator: The callback URL of your hackmd. (serverurl + /auth/saml/callback)
|
||||
* ACS (Consumer) URL: same as above.
|
||||
* Login URL: login URL(SAML requester) of your hackmd. (serverurl + /auth/saml)
|
||||
|
||||
![onelogin-edit-sp-metadata](../images/auth/onelogin-edit-sp-metadata.png)
|
||||
|
||||
7. The registration is completed. Next, click **SSO** and copy or download the items below.
|
||||
* X.509 Certificate: Click **View Details** and **DOWNLOAD** or copy the content of certificate ....(A)
|
||||
* SAML 2.0 Endpoint (HTTP): Copy the URL ....(B)
|
||||
|
||||
![onelogin-copy-idp-metadata](../images/auth/onelogin-copy-idp-metadata.png)
|
||||
|
||||
8. In your hackmd server, create IdP certificate file from (A)
|
||||
9. Add the IdP URL (B) and the Idp certificate file path to your config.json file or pass them as environment variables.
|
||||
* config.json:
|
||||
````javascript
|
||||
{
|
||||
"production": {
|
||||
"saml": {
|
||||
"idpSsoUrl": "https://*******.onelogin.com/trust/saml2/http-post/sso/******",
|
||||
"idpCert": "/path/to/idp_cert.pem"
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
* environment variables
|
||||
````
|
||||
HMD_SAML_IDPSSOURL=https://*******.onelogin.com/trust/saml2/http-post/sso/******
|
||||
HMD_SAML_IDPCERT=/path/to/idp_cert.pem
|
||||
````
|
||||
10. Try sign-in with SAML from your hackmd sign-in button or OneLogin dashboard (like the screenshot below).
|
||||
|
||||
![onelogin-use-dashboard](../images/auth/onelogin-use-dashboard.png)
|
|
@ -1,85 +0,0 @@
|
|||
Authentication guide - SAML
|
||||
===
|
||||
|
||||
***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!*
|
||||
|
||||
The basic procedure is the same as the case of OneLogin which is mentioned in [OneLogin-Guide](./saml-onelogin.md). If you want to match your IdP, you can use more configurations as below.
|
||||
|
||||
* If your IdP accepts metadata XML of the service provider to ease configuration, use this url to download metadata XML.
|
||||
* {{your-serverurl}}/auth/saml/metadata
|
||||
* _Note: If not accessible from IdP, download to local once and upload to IdP._
|
||||
* Change the value of `issuer`, `identifierFormat` to match your IdP.
|
||||
* `issuer`: A unique id to identify the application to the IdP, which is the base URL of your HackMD as default
|
||||
* `identifierFormat`: A format of unique id to identify the user of IdP, which is the format based on email address as default. It is recommend that you use as below.
|
||||
* urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress (default)
|
||||
* urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
* config.json:
|
||||
````javascript
|
||||
{
|
||||
"production": {
|
||||
"saml": {
|
||||
/* omitted */
|
||||
"issuer": "myhackmd"
|
||||
"identifierFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
* environment variables
|
||||
````
|
||||
HMD_SAML_ISSUER=myhackmd
|
||||
HMD_SAML_IDENTIFIERFORMAT=urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
|
||||
````
|
||||
|
||||
* Change mapping of attribute names to customize the displaying user name and email address to match your IdP.
|
||||
* `attribute`: A dictionary to map attribute names
|
||||
* `attribute.id`: A primary key of user table for your HackMD
|
||||
* `attribute.username`: Attribute name of displaying user name on HackMD
|
||||
* `attribute.email`: Attribute name of email address, which will be also used for Gravatar
|
||||
* _Note: Default value of all attributes is NameID of SAML response, which is email address if `identifierFormat` is default._
|
||||
* config.json:
|
||||
````javascript
|
||||
{
|
||||
"production": {
|
||||
"saml": {
|
||||
/* omitted */
|
||||
"attribute": {
|
||||
"id": "sAMAccountName",
|
||||
"username": "displayName",
|
||||
"email": "mail"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
* environment variables
|
||||
````
|
||||
HMD_SAML_ATTRIBUTE_ID=sAMAccountName
|
||||
HMD_SAML_ATTRIBUTE_USERNAME=nickName
|
||||
HMD_SAML_ATTRIBUTE_EMAIL=mail
|
||||
````
|
||||
|
||||
* If you want to control permission by group membership, add group attribute name and required group (allowed) or external group (not allowed).
|
||||
* `groupAttribute`: An attribute name of group membership
|
||||
* `requiredGroups`: Group names array for allowed access to HackMD. Use vertical bar to separate for environment variables.
|
||||
* `externalGroups`: Group names array for not allowed access to HackMD. Use vertical bar to separate for environment variables.
|
||||
* _Note: Evaluates `externalGroups` first_
|
||||
* config.json:
|
||||
````javascript
|
||||
{
|
||||
"production": {
|
||||
"saml": {
|
||||
/* omitted */
|
||||
"groupAttribute": "memberOf",
|
||||
"requiredGroups": [ "hackmd-users", "board-members" ],
|
||||
"externalGroups": [ "temporary-staff" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
* environment variables
|
||||
````
|
||||
HMD_SAML_GROUPATTRIBUTE=memberOf
|
||||
HMD_SAML_REQUIREDGROUPS=hackmd-users|board-members
|
||||
HMD_SAML_EXTERNALGROUPS=temporary-staff
|
||||
````
|
|
@ -1,44 +0,0 @@
|
|||
Authentication guide - Twitter
|
||||
===
|
||||
|
||||
***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!*
|
||||
|
||||
1. Sign-in or sign-up for a Twitter account
|
||||
2. Go to the Twitter Application management page [here](https://apps.twitter.com/)
|
||||
3. Click on the **Create New App** button to create a new Twitter app:
|
||||
|
||||
![create-twitter-app](../images/auth/create-twitter-app.png)
|
||||
|
||||
4. Fill out the create application form, check the developer agreement box, and click **Create Your Twitter Application**
|
||||
|
||||
![register-twitter-application](../images/auth/register-twitter-application.png)
|
||||
|
||||
*Note: you may have to register your phone number with Twitter to create a Twitter application*
|
||||
|
||||
To do this Click your profile icon --> Settings and privacy --> Mobile --> Select Country/region --> Enter phone number --> Click Continue
|
||||
|
||||
5. After you receive confirmation that the Twitter application was created, click **Keys and Access Tokens**
|
||||
|
||||
![twitter-app-confirmation](../images/auth/twitter-app-confirmation.png)
|
||||
|
||||
6. Obtain your Twitter Consumer Key and Consumer Secret
|
||||
|
||||
![twitter-app-keys](../images/auth/twitter-app-keys.png)
|
||||
|
||||
7. Add your Consumer Key and Consumer Secret to your config.json file or pass them as environment variables:
|
||||
* config.json:
|
||||
````javascript
|
||||
{
|
||||
"production": {
|
||||
"twitter": {
|
||||
"consumerKey": "esTCJFXXXXXXXXXXXXXXXXXXX",
|
||||
"consumerSecret": "zpCs4tU86pRVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
* environment variables:
|
||||
````
|
||||
HMD_TWITTER_CONSUMERKEY=esTCJFXXXXXXXXXXXXXXXXXXX
|
||||
HMD_TWITTER_CONSUMERSECRET=zpCs4tU86pRVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
````
|
Before Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 234 KiB |
Before Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 198 KiB |
Before Width: | Height: | Size: 187 KiB |
Before Width: | Height: | Size: 159 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 98 KiB |
|
@ -1,131 +0,0 @@
|
|||
Pad migration guide from etherpad-lite
|
||||
===
|
||||
|
||||
The goal of this migration is to do a "dumb" import from all the pads in Etherpad, to notes in
|
||||
CodiMD. In particular, the url locations of the pads in Etherpad will be lost. Furthermore, any
|
||||
metadata in Etherpad, such as revisions, author data and also formatted text will not be migrated
|
||||
to CodiMD (only the plain text contents).
|
||||
|
||||
Note that this guide is not really meant as a support guide. I migrated my own Etherpad to CodiMD,
|
||||
and it turned out to be quite easy in my opinion. In this guide I share my experience. Stuff may
|
||||
require some creativity to work properly in your case. When I wrote this guide, I was using
|
||||
[Etherpad 1.7.0] and [CodiMD 1.2.1]. Good luck!
|
||||
|
||||
[Etherpad 1.7.0]: https://github.com/ether/etherpad-lite/tree/1.7.0
|
||||
[CodiMD 1.2.1]: https://github.com/hackmdio/codimd/tree/1.2.1
|
||||
|
||||
## 0. Requirements
|
||||
|
||||
- `curl`
|
||||
- running Etherpad server
|
||||
- running CodiMD server
|
||||
- [codimd-cli]
|
||||
|
||||
[codimd-cli]: https://github.com/hackmdio/codimd-cli/blob/master/bin/codimd
|
||||
|
||||
## 1. Retrieve the list of pads
|
||||
|
||||
First, compose a list of all the pads that you want to have migrated from your Etherpad. Other than
|
||||
the admin interface, Etherpad does not have a dedicated function to dump a list of all the pads.
|
||||
However, the Etherpad wiki explains how to list all the pads by [talking directly to the
|
||||
database][howtolistallpads].
|
||||
|
||||
You will end up with a file containing a pad name on each line:
|
||||
|
||||
```
|
||||
date-ideas
|
||||
groceries
|
||||
london
|
||||
weddingchecklist
|
||||
(...)
|
||||
```
|
||||
|
||||
[howtolistallpads]: https://github.com/ether/etherpad-lite/wiki/How-to-list-all-pads/49701ecdcbe07aea7ad27ffa23aed0d99c2e17db
|
||||
|
||||
## 2. Run the migration
|
||||
|
||||
Download [codimd-cli] and put the script in the same directory as the file containing the pad names.
|
||||
Add to this directory the file listed below, I called it `migrate-etherpad.sh`. Modify at least the
|
||||
configuration settings `ETHERPAD_SERVER` and `CODIMD_SERVER`.
|
||||
|
||||
```shell
|
||||
#!/bin/sh
|
||||
|
||||
# migrate-etherpad.sh
|
||||
#
|
||||
# Description: Migrate pads from etherpad to codimd
|
||||
# Author: Daan Sprenkels <hello@dsprenkels.com>
|
||||
|
||||
# This script uses the codimd command line script[1] to import a list of pads from
|
||||
# [1]: https://github.com/hackmdio/codimd-cli/blob/master/bin/codimd
|
||||
|
||||
# The base url to where etherpad is hosted
|
||||
ETHERPAD_SERVER="https://etherpad.example.com"
|
||||
|
||||
# The base url where codimd is hosted
|
||||
CODIMD_SERVER="https://codimd.example.com"
|
||||
|
||||
# Write a list of pads and the urls which they were migrated to
|
||||
REDIRECTS_FILE="redirects.txt"
|
||||
|
||||
|
||||
# Fail if not called correctly
|
||||
if (( $# != 1 )); then
|
||||
echo "Usage: $0 PAD_NAMES_FILE"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Do the migration
|
||||
for PAD_NAME in $1; do
|
||||
# Download the pad
|
||||
PAD_FILE="$(mktemp)"
|
||||
curl "$ETHERPAD_SERVER/p/$PAD_NAME/export/txt" >"$PAD_FILE"
|
||||
|
||||
# Import the pad into codimd
|
||||
OUTPUT="$(./codimd import "$PAD_FILE")"
|
||||
echo "$PAD_NAME -> $OUTPUT" >>"$REDIRECTS_FILE"
|
||||
done
|
||||
```
|
||||
|
||||
Call this file like this:
|
||||
|
||||
```shell
|
||||
./migrate-etherpad.sh pad_names.txt
|
||||
```
|
||||
|
||||
This will download all the pads in `pad_names.txt` and put them on CodiMD. They will get assigned
|
||||
random ids, so you won't be able to find them. The script will save the mappings to a file though
|
||||
(in my case `redirects.txt`). You can use this file to redirect your users when they visit your
|
||||
etherpad using a `301 Permanent Redirect` status code (see the next section).
|
||||
|
||||
## 3. Setup redirects (optional)
|
||||
|
||||
I got a `redirects.txt` file that looked a bit like this:
|
||||
|
||||
```
|
||||
date-ideas -> Found. Redirecting to https://codimd.example.com/mPt0KfiKSBOTQ3mNcdfn
|
||||
groceries -> Found. Redirecting to https://codimd.example.com/UukqgwLfhYyUUtARlcJ2_y
|
||||
london -> Found. Redirecting to https://codimd.example.com/_d3wa-BE8t4Swv5w7O2_9R
|
||||
weddingchecklist -> Found. Redirecting to https://codimd.example.com/XcQGqlBjl0u40wfT0N8TzQ
|
||||
(...)
|
||||
```
|
||||
|
||||
Using some `sed` magic, I changed it to an nginx config snippet:
|
||||
|
||||
```
|
||||
location = /p/date-ideas {
|
||||
return 301 https://codimd.example.com/mPt0M1KfiKSBOTQ3mNcdfn;
|
||||
}
|
||||
location = /p/groceries {
|
||||
return 301 https://codimd.example.com/UukqgwLfhYyUUtARlcJ2_y;
|
||||
}
|
||||
location = /p/london {
|
||||
return 301 https://codimd.example.com/_d3wa-BE8t4Swv5w7O2_9R;
|
||||
}
|
||||
location = /p/weddingchecklist {
|
||||
return 301 https://codimd.example.com/XcQGqlBjl0u40wfT0N8TzQ;
|
||||
}
|
||||
```
|
||||
|
||||
I put this file into my `etherpad.example.com` nginx config, such that all the users would be
|
||||
redirected accordingly.
|
|
@ -1,85 +0,0 @@
|
|||
Minio Guide for CodiMD
|
||||
===
|
||||
|
||||
***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!*
|
||||
|
||||
1. First of all you need to setup Minio itself.
|
||||
|
||||
Please refer to the [official Minio docs](https://docs.minio.io/) for an
|
||||
production setup.
|
||||
|
||||
For checking it out and development purposes a non-persistent setup is enough:
|
||||
```console
|
||||
docker run --name test-minio --rm -d -p 9000:9000 minio/minio server /data
|
||||
```
|
||||
|
||||
*Please notice this is not for productive use as all your data gets lost
|
||||
when you stop this container*
|
||||
|
||||
2. Next step is to get the credentials form the container:
|
||||
|
||||
```
|
||||
docker logs test-minio
|
||||
```
|
||||
|
||||
![docker logs](images/minio-image-upload/docker-logs.png)
|
||||
|
||||
3. Open http://localhost:9000 and login with the shown credentials.
|
||||
|
||||
![minio default view](images/minio-image-upload/default-view.png)
|
||||
|
||||
4. Create a bucket for HackMD
|
||||
|
||||
![minio create bucket](images/minio-image-upload/create-bucket.png)
|
||||
|
||||
5. Add a policy for the prefix `uploads` and make it read-only.
|
||||
|
||||
![minio edit policy](images/minio-image-upload/open-edit-policy.png)
|
||||
*Open policy editor*
|
||||
|
||||
![minio policy adding](images/minio-image-upload/create-policy.png)
|
||||
*Add policy for uploads*
|
||||
|
||||
6. Set credentials and configs for Minio in HackMD's `config.json`
|
||||
|
||||
```JSON
|
||||
"minio": {
|
||||
"accessKey": "888MXJ7EP4XXXXXXXXX",
|
||||
"secretKey": "yQS2EbM1Y6IJrp/1BUKWq2/XXXXXXXXXXXXXXX",
|
||||
"endPoint": "localhost",
|
||||
"port": 9000,
|
||||
"secure": false
|
||||
}
|
||||
```
|
||||
*You have to use different values for `endpoint` and `port` for a production
|
||||
setup. Keep in mind the `endpoint`-address has to be public accessible from
|
||||
your browser.*
|
||||
|
||||
7. Set bucket name
|
||||
|
||||
```JSON
|
||||
"s3bucket": "hackmd"
|
||||
```
|
||||
|
||||
8. Set upload type.
|
||||
|
||||
```JSON
|
||||
"imageuploadtype": "minio"
|
||||
```
|
||||
|
||||
9. Review your config.
|
||||
|
||||
```json
|
||||
{
|
||||
// all your other config…
|
||||
"minio": {
|
||||
"accessKey": "888MXJ7EP4XXXXXXXXX",
|
||||
"secretKey": "yQS2EbM1Y6IJrp/1BUKWq2/XXXXXXXXXXXXXXX",
|
||||
"endPoint": "localhost",
|
||||
"port": 9000,
|
||||
"secure": false
|
||||
},
|
||||
"s3bucket": "hackmd",
|
||||
"imageuploadtype": "minio"
|
||||
}
|
||||
```
|
|
@ -1,17 +0,0 @@
|
|||
Setup your terms of use
|
||||
===
|
||||
|
||||
To setup your terms of use, you need to provide a document called `terms-of-use.md` which contains them. Of course written in Markdown.
|
||||
|
||||
It has to be provided under `./public/docs/` and will be automatically turned into a CodiMD document. It will also automatically updated as soon as you change the document on disk.
|
||||
|
||||
As soon as the file exists a link will show up in the bottom part along with the release notes and link to them.
|
||||
|
||||
Setup your privacy policy
|
||||
===
|
||||
|
||||
To add a privacy policy you can use the same technique as for the terms of use. The main difference is that the document is called `privacy.md`.
|
||||
|
||||
See our example file `./public/docs/privacy.md.example` container some useful hints for writing your own privacy policy.
|
||||
|
||||
As with the terms of use, a link to the privacy notices will show up in the area where the release notes are provided on the index page.
|
|
@ -1,83 +0,0 @@
|
|||
# Guide - Setup CodiMD S3 image upload
|
||||
|
||||
***Note:** This guide was written before the renaming. Just replace `HackMD` with `CodiMD` in your mind :smile: thanks!*
|
||||
|
||||
1. Go to [AWS S3 console](https://console.aws.amazon.com/s3/home) and create a new bucket.
|
||||
|
||||
![create-bucket](images/s3-image-upload/create-bucket.png)
|
||||
|
||||
2. Click on bucket, select **Properties** on the side panel, and find **Permission** section. Click **Edit bucket policy**.
|
||||
|
||||
![bucket-property](images/s3-image-upload/bucket-property.png)
|
||||
|
||||
3. Enter the following policy, replace `bucket_name` with your bucket name:
|
||||
|
||||
![bucket-policy-editor](images/s3-image-upload/bucket-policy-editor.png)
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": "arn:aws:s3:::bucket_name/uploads/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
4. Go to IAM console and create a new IAM user. Remember your user credentials(`key`/`access token`)
|
||||
|
||||
5. Enter user page, select **Permission** tab, look at **Inline Policies** section, and click **Create User Policy**
|
||||
|
||||
![iam-user](images/s3-image-upload/iam-user.png)
|
||||
|
||||
6. Select **Custom Policy**
|
||||
|
||||
![custom-policy](images/s3-image-upload/custom-policy.png)
|
||||
|
||||
7. Enter the following policy, replace `bucket_name` with your bucket name:
|
||||
|
||||
![review-policy](images/s3-image-upload/review-policy.png)
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::bucket_name/uploads/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
8. Edit `config.json` and set following keys:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"production": {
|
||||
...
|
||||
"imageuploadtype": "s3",
|
||||
"s3": {
|
||||
"accessKeyId": "YOUR_S3_ACCESS_KEY_ID",
|
||||
"secretAccessKey": "YOUR_S3_ACCESS_KEY",
|
||||
"region": "YOUR_S3_REGION" // example: ap-northeast-1
|
||||
},
|
||||
"s3bucket": "YOUR_S3_BUCKET_NAME"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
9. In additional to edit `config.json` directly, you could also try [environment variable](https://github.com/hackmdio/hackmd#environment-variables-will-overwrite-other-server-configs).
|
||||
|
||||
## Related Tools
|
||||
|
||||
* [AWS Policy Generator](http://awspolicygen.s3.amazonaws.com/policygen.html)
|
|
@ -0,0 +1,28 @@
|
|||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const BitbucketStrategy = require('passport-bitbucket-oauth2').Strategy
|
||||
const config = require('../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
const bitbucketAuth = module.exports = Router()
|
||||
|
||||
passport.use(new BitbucketStrategy({
|
||||
clientID: config.bitbucket.clientID,
|
||||
clientSecret: config.bitbucket.clientSecret,
|
||||
callbackURL: config.serverURL + '/auth/bitbucket/callback'
|
||||
}, passportGeneralCallback))
|
||||
|
||||
bitbucketAuth.get('/auth/bitbucket', function (req, res, next) {
|
||||
setReturnToFromReferer(req)
|
||||
passport.authenticate('bitbucket')(req, res, next)
|
||||
})
|
||||
|
||||
// bitbucket auth callback
|
||||
bitbucketAuth.get('/auth/bitbucket/callback',
|
||||
passport.authenticate('bitbucket', {
|
||||
successReturnToOrRedirect: config.serverURL + '/',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
|
@ -3,7 +3,7 @@
|
|||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
|
||||
const config = require('../../../config')
|
||||
const config = require('../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
const dropboxAuth = module.exports = Router()
|
|
@ -4,12 +4,12 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
const validator = require('validator')
|
||||
const LocalStrategy = require('passport-local').Strategy
|
||||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const config = require('../../config')
|
||||
const models = require('../../models')
|
||||
const logger = require('../../logger')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const response = require('../../../response')
|
||||
const response = require('../../response')
|
||||
|
||||
const emailAuth = module.exports = Router()
|
||||
|
||||
|
@ -33,8 +33,8 @@ passport.use(new LocalStrategy({
|
|||
|
||||
if (config.allowEmailRegister) {
|
||||
emailAuth.post('/register', urlencodedParser, function (req, res, next) {
|
||||
if (!req.body.email || !req.body.password) return response.errorBadRequest(res)
|
||||
if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res)
|
||||
if (!req.body.email || !req.body.password) return response.errorBadRequest(req, res)
|
||||
if (!validator.isEmail(req.body.email)) return response.errorBadRequest(req, res)
|
||||
models.User.findOrCreate({
|
||||
where: {
|
||||
email: req.body.email
|
||||
|
@ -57,14 +57,14 @@ if (config.allowEmailRegister) {
|
|||
return res.redirect(config.serverURL + '/')
|
||||
}).catch(function (err) {
|
||||
logger.error('auth callback failed: ' + err)
|
||||
return response.errorInternalError(res)
|
||||
return response.errorInternalError(req, res)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
emailAuth.post('/login', urlencodedParser, function (req, res, next) {
|
||||
if (!req.body.email || !req.body.password) return response.errorBadRequest(res)
|
||||
if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res)
|
||||
if (!req.body.email || !req.body.password) return response.errorBadRequest(req, res)
|
||||
if (!validator.isEmail(req.body.email)) return response.errorBadRequest(req, res)
|
||||
setReturnToFromReferer(req)
|
||||
passport.authenticate('local', {
|
||||
successReturnToOrRedirect: config.serverURL + '/',
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
const FacebookStrategy = require('passport-facebook').Strategy
|
||||
|
||||
const config = require('../../../config')
|
||||
const config = require('../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
const facebookAuth = module.exports = Router()
|
|
@ -3,8 +3,8 @@
|
|||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const GithubStrategy = require('passport-github').Strategy
|
||||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const config = require('../../config')
|
||||
const response = require('../../response')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
const { URL } = require('url')
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const GitlabStrategy = require('passport-gitlab2').Strategy
|
||||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const config = require('../../config')
|
||||
const response = require('../../response')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
const HttpsProxyAgent = require('https-proxy-agent')
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
var GoogleStrategy = require('passport-google-oauth20').Strategy
|
||||
const config = require('../../../config')
|
||||
const config = require('../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
const googleAuth = module.exports = Router()
|
||||
|
@ -17,7 +17,10 @@ passport.use(new GoogleStrategy({
|
|||
|
||||
googleAuth.get('/auth/google', function (req, res, next) {
|
||||
setReturnToFromReferer(req)
|
||||
passport.authenticate('google', { scope: ['profile'] })(req, res, next)
|
||||
passport.authenticate('google', {
|
||||
scope: ['profile'],
|
||||
hostedDomain: config.google.hostedDomain
|
||||
})(req, res, next)
|
||||
})
|
||||
// google auth callback
|
||||
googleAuth.get('/auth/google/callback',
|
|
@ -3,9 +3,9 @@
|
|||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const models = require('../../models')
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
const models = require('../models')
|
||||
|
||||
const authRouter = module.exports = Router()
|
||||
|
||||
|
@ -37,6 +37,7 @@ passport.deserializeUser(function (id, done) {
|
|||
if (config.isFacebookEnable) authRouter.use(require('./facebook'))
|
||||
if (config.isTwitterEnable) authRouter.use(require('./twitter'))
|
||||
if (config.isGitHubEnable) authRouter.use(require('./github'))
|
||||
if (config.isBitbucketEnable) authRouter.use(require('./bitbucket'))
|
||||
if (config.isGitLabEnable) authRouter.use(require('./gitlab'))
|
||||
if (config.isMattermostEnable) authRouter.use(require('./mattermost'))
|
||||
if (config.isDropboxEnable) authRouter.use(require('./dropbox'))
|
|
@ -3,12 +3,12 @@
|
|||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const LDAPStrategy = require('passport-ldapauth')
|
||||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const config = require('../../config')
|
||||
const models = require('../../models')
|
||||
const logger = require('../../logger')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const response = require('../../../response')
|
||||
const response = require('../../response')
|
||||
|
||||
const ldapAuth = module.exports = Router()
|
||||
|
||||
|
@ -81,7 +81,7 @@ passport.use(new LDAPStrategy({
|
|||
}))
|
||||
|
||||
ldapAuth.post('/auth/ldap', urlencodedParser, function (req, res, next) {
|
||||
if (!req.body.username || !req.body.password) return response.errorBadRequest(res)
|
||||
if (!req.body.username || !req.body.password) return response.errorBadRequest(req, res)
|
||||
setReturnToFromReferer(req)
|
||||
passport.authenticate('ldapauth', {
|
||||
successReturnToOrRedirect: config.serverURL + '/',
|
|
@ -5,7 +5,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
const MattermostClient = require('mattermost-redux/client/client4').default
|
||||
const OAuthStrategy = require('passport-oauth2').Strategy
|
||||
const config = require('../../../config')
|
||||
const config = require('../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
const mattermostAuth = module.exports = Router()
|
|
@ -0,0 +1,33 @@
|
|||
'use strict'
|
||||
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
|
||||
const config = require('../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
const { OAuth2CustomStrategy } = require('./strategy')
|
||||
|
||||
const oauth2Auth = module.exports = Router()
|
||||
|
||||
passport.use(new OAuth2CustomStrategy({
|
||||
authorizationURL: config.oauth2.authorizationURL,
|
||||
tokenURL: config.oauth2.tokenURL,
|
||||
clientID: config.oauth2.clientID,
|
||||
clientSecret: config.oauth2.clientSecret,
|
||||
callbackURL: config.serverURL + '/auth/oauth2/callback',
|
||||
userProfileURL: config.oauth2.userProfileURL,
|
||||
scope: config.oauth2.scope
|
||||
}, passportGeneralCallback))
|
||||
|
||||
oauth2Auth.get('/auth/oauth2', function (req, res, next) {
|
||||
setReturnToFromReferer(req)
|
||||
passport.authenticate('oauth2')(req, res, next)
|
||||
})
|
||||
|
||||
// github auth callback
|
||||
oauth2Auth.get('/auth/oauth2/callback',
|
||||
passport.authenticate('oauth2', {
|
||||
successReturnToOrRedirect: config.serverURL + '/',
|
||||
failureRedirect: config.serverURL + '/'
|
||||
})
|
||||
)
|
|
@ -0,0 +1,74 @@
|
|||
'use strict'
|
||||
|
||||
const { Strategy, InternalOAuthError } = require('passport-oauth2')
|
||||
const config = require('../../config')
|
||||
|
||||
function parseProfile (data) {
|
||||
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
|
||||
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
|
||||
const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)
|
||||
|
||||
if (!username) {
|
||||
throw new Error('cannot fetch username: please set correct CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR')
|
||||
}
|
||||
|
||||
return {
|
||||
id: username,
|
||||
username: username,
|
||||
displayName: displayName,
|
||||
email: email
|
||||
}
|
||||
}
|
||||
|
||||
function extractProfileAttribute (data, path) {
|
||||
if (!data) return undefined
|
||||
if (typeof path !== 'string') return undefined
|
||||
// can handle stuff like `attrs[0].name`
|
||||
path = path.split('.')
|
||||
for (const segment of path) {
|
||||
const m = segment.match(/([\d\w]+)\[(.*)\]/)
|
||||
if (!m) {
|
||||
data = data[segment]
|
||||
} else {
|
||||
if (m.length < 3) return undefined
|
||||
if (!data[m[1]]) return undefined
|
||||
data = data[m[1]][m[2]]
|
||||
}
|
||||
if (!data) return undefined
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
class OAuth2CustomStrategy extends Strategy {
|
||||
constructor (options, verify) {
|
||||
options.customHeaders = options.customHeaders || {}
|
||||
super(options, verify)
|
||||
this.name = 'oauth2'
|
||||
this._userProfileURL = options.userProfileURL
|
||||
this._oauth2.useAuthorizationHeaderforGET(true)
|
||||
}
|
||||
|
||||
userProfile (accessToken, done) {
|
||||
this._oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
|
||||
if (err) {
|
||||
return done(new InternalOAuthError('Failed to fetch user profile', err))
|
||||
}
|
||||
|
||||
let profile, json
|
||||
try {
|
||||
json = JSON.parse(body)
|
||||
profile = parseProfile(json)
|
||||
} catch (ex) {
|
||||
return done(new InternalOAuthError('Failed to parse user profile' + ex.toString()))
|
||||
}
|
||||
|
||||
profile.provider = 'oauth2'
|
||||
|
||||
done(null, profile)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.OAuth2CustomStrategy = OAuth2CustomStrategy
|
||||
exports.parseProfile = parseProfile
|
||||
exports.extractProfileAttribute = extractProfileAttribute
|
|
@ -3,9 +3,9 @@
|
|||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
|
||||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const config = require('../../config')
|
||||
const models = require('../../models')
|
||||
const logger = require('../../logger')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const SamlStrategy = require('passport-saml').Strategy
|
||||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const config = require('../../config')
|
||||
const models = require('../../models')
|
||||
const logger = require('../../logger')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const fs = require('fs')
|
||||
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
const TwitterStrategy = require('passport-twitter').Strategy
|
||||
|
||||
const config = require('../../../config')
|
||||
const config = require('../../config')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
const twitterAuth = module.exports = Router()
|
|
@ -1,8 +1,8 @@
|
|||
'use strict'
|
||||
|
||||
const models = require('../../models')
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const models = require('../models')
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
|
||||
exports.setReturnToFromReferer = function setReturnToFromReferer (req) {
|
||||
var referer = req.get('referer')
|
|
@ -14,7 +14,7 @@ module.exports = {
|
|||
hsts: {
|
||||
enable: true,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365,
|
||||
includeSubdomains: true,
|
||||
includeSubdomains: false,
|
||||
preload: true
|
||||
},
|
||||
csp: {
|
||||
|
@ -29,8 +29,8 @@ module.exports = {
|
|||
},
|
||||
protocolUseSSL: false,
|
||||
useCDN: true,
|
||||
allowAnonymous: true,
|
||||
allowAnonymousEdits: false,
|
||||
allowAnonymous: false,
|
||||
allowAnonymousEdits: true,
|
||||
allowAnonymousViews: true,
|
||||
allowFreeURL: false,
|
||||
forbiddenNoteIDs: ['robots.txt', 'favicon.ico', 'api'],
|
||||
|
@ -93,7 +93,13 @@ module.exports = {
|
|||
authorizationURL: undefined,
|
||||
tokenURL: undefined,
|
||||
clientID: undefined,
|
||||
clientSecret: undefined
|
||||
clientSecret: undefined,
|
||||
baseURL: undefined,
|
||||
userProfileURL: undefined,
|
||||
userProfileUsernameAttr: 'username',
|
||||
userProfileDisplayNameAttr: 'displayName',
|
||||
userProfileEmailAttr: 'email',
|
||||
scope: 'email'
|
||||
},
|
||||
facebook: {
|
||||
clientID: undefined,
|
||||
|
@ -127,7 +133,8 @@ module.exports = {
|
|||
},
|
||||
google: {
|
||||
clientID: undefined,
|
||||
clientSecret: undefined
|
||||
clientSecret: undefined,
|
||||
hostedDomain: undefined
|
||||
},
|
||||
ldap: {
|
||||
providerName: undefined,
|
||||
|
@ -178,5 +185,6 @@ module.exports = {
|
|||
// Generated id: "31-good-morning-my-friend---do-you-have-5"
|
||||
// 2nd appearance: "31-good-morning-my-friend---do-you-have-5-1"
|
||||
// 3rd appearance: "31-good-morning-my-friend---do-you-have-5-2"
|
||||
linkifyHeaderStyle: 'keep-case'
|
||||
linkifyHeaderStyle: 'keep-case',
|
||||
autoVersionCheck: true
|
||||
}
|
||||
|
|
|
@ -70,6 +70,10 @@ module.exports = {
|
|||
clientID: process.env.CMD_GITHUB_CLIENTID,
|
||||
clientSecret: process.env.CMD_GITHUB_CLIENTSECRET
|
||||
},
|
||||
bitbucket: {
|
||||
clientID: process.env.CMD_BITBUCKET_CLIENTID,
|
||||
clientSecret: process.env.CMD_BITBUCKET_CLIENTSECRET
|
||||
},
|
||||
gitlab: {
|
||||
baseURL: process.env.CMD_GITLAB_BASEURL,
|
||||
clientID: process.env.CMD_GITLAB_CLIENTID,
|
||||
|
@ -84,14 +88,15 @@ module.exports = {
|
|||
oauth2: {
|
||||
providerName: process.env.CMD_OAUTH2_PROVIDERNAME,
|
||||
baseURL: process.env.CMD_OAUTH2_BASEURL,
|
||||
clientID: process.env.CMD_OAUTH2_CLIENT_ID,
|
||||
clientSecret: process.env.CMD_OAUTH2_CLIENT_SECRET,
|
||||
authorizationURL: process.env.CMD_OAUTH2_AUTHORIZATION_URL,
|
||||
tokenURL: process.env.CMD_OAUTH2_TOKEN_URL,
|
||||
userProfileURL: process.env.CMD_OAUTH2_USER_PROFILE_URL,
|
||||
scope: process.env.CMD_OAUTH2_SCOPE,
|
||||
userProfileUsernameAttr: process.env.CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR,
|
||||
userProfileDisplayNameAttr: process.env.CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR,
|
||||
userProfileEmailAttr: process.env.CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR,
|
||||
tokenURL: process.env.CMD_OAUTH2_TOKEN_URL,
|
||||
authorizationURL: process.env.CMD_OAUTH2_AUTHORIZATION_URL,
|
||||
clientID: process.env.CMD_OAUTH2_CLIENT_ID,
|
||||
clientSecret: process.env.CMD_OAUTH2_CLIENT_SECRET
|
||||
userProfileEmailAttr: process.env.CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR
|
||||
},
|
||||
dropbox: {
|
||||
clientID: process.env.CMD_DROPBOX_CLIENTID,
|
||||
|
@ -100,7 +105,8 @@ module.exports = {
|
|||
},
|
||||
google: {
|
||||
clientID: process.env.CMD_GOOGLE_CLIENTID,
|
||||
clientSecret: process.env.CMD_GOOGLE_CLIENTSECRET
|
||||
clientSecret: process.env.CMD_GOOGLE_CLIENTSECRET,
|
||||
hostedDomain: process.env.CMD_GOOGLE_HOSTEDDOMAIN
|
||||
},
|
||||
ldap: {
|
||||
providerName: process.env.CMD_LDAP_PROVIDERNAME,
|
||||
|
@ -138,5 +144,6 @@ module.exports = {
|
|||
allowPDFExport: toBooleanConfig(process.env.CMD_ALLOW_PDF_EXPORT),
|
||||
openID: toBooleanConfig(process.env.CMD_OPENID),
|
||||
defaultUseHardbreak: toBooleanConfig(process.env.CMD_DEFAULT_USE_HARD_BREAK),
|
||||
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE
|
||||
linkifyHeaderStyle: process.env.CMD_LINKIFY_HEADER_STYLE,
|
||||
autoVersionCheck: toBooleanConfig(process.env.CMD_AUTO_VERSION_CHECK)
|
||||
}
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
|
||||
|
||||
module.exports = {
|
||||
domain: process.env.HMD_DOMAIN,
|
||||
urlPath: process.env.HMD_URL_PATH,
|
||||
port: toIntegerConfig(process.env.HMD_PORT),
|
||||
urlAddPort: toBooleanConfig(process.env.HMD_URL_ADDPORT),
|
||||
useSSL: toBooleanConfig(process.env.HMD_USESSL),
|
||||
hsts: {
|
||||
enable: toBooleanConfig(process.env.HMD_HSTS_ENABLE),
|
||||
maxAgeSeconds: toIntegerConfig(process.env.HMD_HSTS_MAX_AGE),
|
||||
includeSubdomains: toBooleanConfig(process.env.HMD_HSTS_INCLUDE_SUBDOMAINS),
|
||||
preload: toBooleanConfig(process.env.HMD_HSTS_PRELOAD)
|
||||
},
|
||||
csp: {
|
||||
enable: toBooleanConfig(process.env.HMD_CSP_ENABLE),
|
||||
reportURI: process.env.HMD_CSP_REPORTURI
|
||||
},
|
||||
protocolUseSSL: toBooleanConfig(process.env.HMD_PROTOCOL_USESSL),
|
||||
allowOrigin: toArrayConfig(process.env.HMD_ALLOW_ORIGIN),
|
||||
useCDN: toBooleanConfig(process.env.HMD_USECDN),
|
||||
allowAnonymous: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS),
|
||||
allowAnonymousEdits: toBooleanConfig(process.env.HMD_ALLOW_ANONYMOUS_EDITS),
|
||||
allowFreeURL: toBooleanConfig(process.env.HMD_ALLOW_FREEURL),
|
||||
defaultPermission: process.env.HMD_DEFAULT_PERMISSION,
|
||||
dbURL: process.env.HMD_DB_URL,
|
||||
sessionSecret: process.env.HMD_SESSION_SECRET,
|
||||
sessionLife: toIntegerConfig(process.env.HMD_SESSION_LIFE),
|
||||
responseMaxLag: toIntegerConfig(process.env.HMD_RESPONSE_MAX_LAG),
|
||||
imageUploadType: process.env.HMD_IMAGE_UPLOAD_TYPE,
|
||||
imgur: {
|
||||
clientID: process.env.HMD_IMGUR_CLIENTID
|
||||
},
|
||||
s3: {
|
||||
accessKeyId: process.env.HMD_S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.HMD_S3_SECRET_ACCESS_KEY,
|
||||
region: process.env.HMD_S3_REGION
|
||||
},
|
||||
minio: {
|
||||
accessKey: process.env.HMD_MINIO_ACCESS_KEY,
|
||||
secretKey: process.env.HMD_MINIO_SECRET_KEY,
|
||||
endPoint: process.env.HMD_MINIO_ENDPOINT,
|
||||
secure: toBooleanConfig(process.env.HMD_MINIO_SECURE),
|
||||
port: toIntegerConfig(process.env.HMD_MINIO_PORT)
|
||||
},
|
||||
s3bucket: process.env.HMD_S3_BUCKET,
|
||||
azure: {
|
||||
connectionString: process.env.HMD_AZURE_CONNECTION_STRING,
|
||||
container: process.env.HMD_AZURE_CONTAINER
|
||||
},
|
||||
facebook: {
|
||||
clientID: process.env.HMD_FACEBOOK_CLIENTID,
|
||||
clientSecret: process.env.HMD_FACEBOOK_CLIENTSECRET
|
||||
},
|
||||
twitter: {
|
||||
consumerKey: process.env.HMD_TWITTER_CONSUMERKEY,
|
||||
consumerSecret: process.env.HMD_TWITTER_CONSUMERSECRET
|
||||
},
|
||||
github: {
|
||||
clientID: process.env.HMD_GITHUB_CLIENTID,
|
||||
clientSecret: process.env.HMD_GITHUB_CLIENTSECRET
|
||||
},
|
||||
gitlab: {
|
||||
baseURL: process.env.HMD_GITLAB_BASEURL,
|
||||
clientID: process.env.HMD_GITLAB_CLIENTID,
|
||||
clientSecret: process.env.HMD_GITLAB_CLIENTSECRET,
|
||||
scope: process.env.HMD_GITLAB_SCOPE
|
||||
},
|
||||
mattermost: {
|
||||
baseURL: process.env.HMD_MATTERMOST_BASEURL,
|
||||
clientID: process.env.HMD_MATTERMOST_CLIENTID,
|
||||
clientSecret: process.env.HMD_MATTERMOST_CLIENTSECRET
|
||||
},
|
||||
oauth2: {
|
||||
baseURL: process.env.HMD_OAUTH2_BASEURL,
|
||||
userProfileURL: process.env.HMD_OAUTH2_USER_PROFILE_URL,
|
||||
userProfileUsernameAttr: process.env.HMD_OAUTH2_USER_PROFILE_USERNAME_ATTR,
|
||||
userProfileDisplayNameAttr: process.env.HMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR,
|
||||
userProfileEmailAttr: process.env.HMD_OAUTH2_USER_PROFILE_EMAIL_ATTR,
|
||||
tokenURL: process.env.HMD_OAUTH2_TOKEN_URL,
|
||||
authorizationURL: process.env.HMD_OAUTH2_AUTHORIZATION_URL,
|
||||
clientID: process.env.HMD_OAUTH2_CLIENT_ID,
|
||||
clientSecret: process.env.HMD_OAUTH2_CLIENT_SECRET
|
||||
},
|
||||
dropbox: {
|
||||
clientID: process.env.HMD_DROPBOX_CLIENTID,
|
||||
clientSecret: process.env.HMD_DROPBOX_CLIENTSECRET,
|
||||
appKey: process.env.HMD_DROPBOX_APPKEY
|
||||
},
|
||||
google: {
|
||||
clientID: process.env.HMD_GOOGLE_CLIENTID,
|
||||
clientSecret: process.env.HMD_GOOGLE_CLIENTSECRET
|
||||
},
|
||||
ldap: {
|
||||
providerName: process.env.HMD_LDAP_PROVIDERNAME,
|
||||
url: process.env.HMD_LDAP_URL,
|
||||
bindDn: process.env.HMD_LDAP_BINDDN,
|
||||
bindCredentials: process.env.HMD_LDAP_BINDCREDENTIALS,
|
||||
searchBase: process.env.HMD_LDAP_SEARCHBASE,
|
||||
searchFilter: process.env.HMD_LDAP_SEARCHFILTER,
|
||||
searchAttributes: toArrayConfig(process.env.HMD_LDAP_SEARCHATTRIBUTES),
|
||||
usernameField: process.env.HMD_LDAP_USERNAMEFIELD,
|
||||
useridField: process.env.HMD_LDAP_USERIDFIELD,
|
||||
tlsca: process.env.HMD_LDAP_TLS_CA
|
||||
},
|
||||
saml: {
|
||||
idpSsoUrl: process.env.HMD_SAML_IDPSSOURL,
|
||||
idpCert: process.env.HMD_SAML_IDPCERT,
|
||||
issuer: process.env.HMD_SAML_ISSUER,
|
||||
identifierFormat: process.env.HMD_SAML_IDENTIFIERFORMAT,
|
||||
disableRequestedAuthnContext: toBooleanConfig(process.env.HMD_SAML_DISABLEREQUESTEDAUTHNCONTEXT),
|
||||
groupAttribute: process.env.HMD_SAML_GROUPATTRIBUTE,
|
||||
externalGroups: toArrayConfig(process.env.HMD_SAML_EXTERNALGROUPS, '|', []),
|
||||
requiredGroups: toArrayConfig(process.env.HMD_SAML_REQUIREDGROUPS, '|', []),
|
||||
attribute: {
|
||||
id: process.env.HMD_SAML_ATTRIBUTE_ID,
|
||||
username: process.env.HMD_SAML_ATTRIBUTE_USERNAME,
|
||||
email: process.env.HMD_SAML_ATTRIBUTE_EMAIL
|
||||
}
|
||||
},
|
||||
email: toBooleanConfig(process.env.HMD_EMAIL),
|
||||
allowEmailRegister: toBooleanConfig(process.env.HMD_ALLOW_EMAIL_REGISTER),
|
||||
allowPDFExport: toBooleanConfig(process.env.HMD_ALLOW_PDF_EXPORT)
|
||||
}
|
|
@ -36,12 +36,9 @@ const fileConfig = fs.existsSync(configFilePath) ? require(configFilePath)[env]
|
|||
|
||||
let config = require('./default')
|
||||
merge(config, require('./defaultSSL'))
|
||||
merge(config, require('./oldDefault'))
|
||||
merge(config, debugConfig)
|
||||
merge(config, packageConfig)
|
||||
merge(config, fileConfig)
|
||||
merge(config, require('./oldEnvironment'))
|
||||
merge(config, require('./hackmdEnvironment'))
|
||||
merge(config, require('./environment'))
|
||||
merge(config, require('./dockerSecret'))
|
||||
|
||||
|
@ -121,6 +118,7 @@ config.isTwitterEnable = config.twitter.consumerKey && config.twitter.consumerSe
|
|||
config.isEmailEnable = config.email
|
||||
config.isOpenIDEnable = config.openID
|
||||
config.isGitHubEnable = config.github.clientID && config.github.clientSecret
|
||||
config.isBitbucketEnable = config.bitbucket.clientID && config.bitbucket.clientSecret
|
||||
config.isGitLabEnable = config.gitlab.clientID && config.gitlab.clientSecret
|
||||
config.isMattermostEnable = config.mattermost.clientID && config.mattermost.clientSecret
|
||||
config.isLDAPEnable = config.ldap.url
|
||||
|
@ -158,7 +156,7 @@ for (let i = keys.length; i--;) {
|
|||
|
||||
// Notify users about the prefix change and inform them they use legacy prefix for environment variables
|
||||
if (Object.keys(process.env).toString().indexOf('HMD_') !== -1) {
|
||||
logger.warn('Using legacy HMD prefix for environment variables. Please change your variables in future. For details see: https://github.com/hackmdio/codimd#environment-variables-will-overwrite-other-server-configs')
|
||||
logger.warn('Using legacy HMD prefix for environment variables. Please change your variables in future. For details see: https://hackmd.io/c/codimd-documentation/%2F%40codimd%2Fmigrate-2-0#1-Drop-old-environment-variables-support')
|
||||
}
|
||||
|
||||
// Generate session secret if it stays on default values
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
urlpath: undefined,
|
||||
urladdport: undefined,
|
||||
alloworigin: undefined,
|
||||
usessl: undefined,
|
||||
protocolusessl: undefined,
|
||||
usecdn: undefined,
|
||||
allowanonymous: undefined,
|
||||
allowanonymousedits: undefined,
|
||||
allowfreeurl: undefined,
|
||||
defaultpermission: undefined,
|
||||
dburl: undefined,
|
||||
// ssl path
|
||||
sslkeypath: undefined,
|
||||
sslcertpath: undefined,
|
||||
sslcapath: undefined,
|
||||
dhparampath: undefined,
|
||||
// other path
|
||||
tmppath: undefined,
|
||||
defaultnotepath: undefined,
|
||||
docspath: undefined,
|
||||
indexpath: undefined,
|
||||
hackmdpath: undefined,
|
||||
errorpath: undefined,
|
||||
prettypath: undefined,
|
||||
slidepath: undefined,
|
||||
// session
|
||||
sessionname: undefined,
|
||||
sessionsecret: undefined,
|
||||
sessionlife: undefined,
|
||||
staticcachetime: undefined,
|
||||
// socket.io
|
||||
heartbeatinterval: undefined,
|
||||
heartbeattimeout: undefined,
|
||||
// document
|
||||
documentmaxlength: undefined,
|
||||
imageuploadtype: undefined,
|
||||
allowemailregister: undefined,
|
||||
allowpdfexport: undefined
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
const { toBooleanConfig } = require('./utils')
|
||||
|
||||
module.exports = {
|
||||
debug: toBooleanConfig(process.env.DEBUG),
|
||||
dburl: process.env.DATABASE_URL,
|
||||
urlpath: process.env.URL_PATH,
|
||||
port: process.env.PORT
|
||||
}
|
13
lib/csp.js
|
@ -7,7 +7,7 @@ var defaultDirectives = {
|
|||
defaultSrc: ['\'self\''],
|
||||
scriptSrc: ['\'self\'', 'vimeo.com', 'https://gist.github.com', 'www.slideshare.net', 'https://query.yahooapis.com', '\'unsafe-eval\''],
|
||||
// ^ TODO: Remove unsafe-eval - webpack script-loader issues https://github.com/hackmdio/codimd/issues/594
|
||||
imgSrc: ['*'],
|
||||
imgSrc: ['*', 'data:'],
|
||||
styleSrc: ['\'self\'', '\'unsafe-inline\'', 'https://github.githubassets.com'], // unsafe-inline is required for some libs, plus used in views
|
||||
fontSrc: ['\'self\'', 'data:', 'https://public.slidesharecdn.com'],
|
||||
objectSrc: ['*'], // Chrome PDF viewer treats PDFs as objects :/
|
||||
|
@ -16,9 +16,13 @@ var defaultDirectives = {
|
|||
connectSrc: ['*']
|
||||
}
|
||||
|
||||
var dropboxDirectives = {
|
||||
scriptSrc: ['https://www.dropbox.com']
|
||||
}
|
||||
|
||||
var cdnDirectives = {
|
||||
scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.mathjax.org'],
|
||||
styleSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.googleapis.com'],
|
||||
scriptSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.jsdelivr.net', 'https://cdn.mathjax.org'],
|
||||
styleSrc: ['https://cdnjs.cloudflare.com', 'https://cdn.jsdelivr.net', 'https://fonts.googleapis.com'],
|
||||
fontSrc: ['https://cdnjs.cloudflare.com', 'https://fonts.gstatic.com']
|
||||
}
|
||||
|
||||
|
@ -37,6 +41,7 @@ CspStrategy.computeDirectives = function () {
|
|||
mergeDirectives(directives, config.csp.directives)
|
||||
mergeDirectivesIf(config.csp.addDefaults, directives, defaultDirectives)
|
||||
mergeDirectivesIf(config.useCDN, directives, cdnDirectives)
|
||||
mergeDirectivesIf(config.dropbox && config.dropbox.appKey, directives, dropboxDirectives)
|
||||
mergeDirectivesIf(config.csp.addDisqus, directives, disqusDirectives)
|
||||
mergeDirectivesIf(config.csp.addGoogleAnalytics, directives, googleAnalyticsDirectives)
|
||||
if (!areAllInlineScriptsAllowed(directives)) {
|
||||
|
@ -71,7 +76,7 @@ function addInlineScriptExceptions (directives) {
|
|||
directives.scriptSrc.push(getCspNonce)
|
||||
// TODO: This is the SHA-256 hash of the inline script in build/reveal.js/plugins/notes/notes.html
|
||||
// Any more clean solution appreciated.
|
||||
directives.scriptSrc.push('\'sha256-Lc+VnBdinzYTTAkFrIoUqdoA9EQFeS1AF9ybmF+LLfM=\'')
|
||||
directives.scriptSrc.push('\'sha256-81acLZNZISnyGYZrSuoYhpzwDTTxi7vC1YM4uNxqWaM=\'')
|
||||
}
|
||||
|
||||
function getCspNonce (req, res) {
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
'use strict'
|
||||
|
||||
const config = require('../config')
|
||||
const { responseError } = require('../response')
|
||||
|
||||
exports.errorForbidden = (req, res) => {
|
||||
if (req.user) {
|
||||
return responseError(res, '403', 'Forbidden', 'oh no.')
|
||||
}
|
||||
|
||||
req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
|
||||
res.redirect(config.serverURL + '/')
|
||||
}
|
||||
|
||||
exports.errorNotFound = (req, res) => {
|
||||
responseError(res, '404', 'Not Found', 'oops.')
|
||||
}
|
||||
|
||||
exports.errorInternalError = (req, res) => {
|
||||
responseError(res, '500', 'Internal Error', 'wtf.')
|
||||
}
|
|
@ -4,10 +4,10 @@
|
|||
var LZString = require('@hackmd/lz-string')
|
||||
|
||||
// core
|
||||
var config = require('./config')
|
||||
var logger = require('./logger')
|
||||
var response = require('./response')
|
||||
var models = require('./models')
|
||||
var config = require('../config')
|
||||
var logger = require('../logger')
|
||||
var response = require('../response')
|
||||
var models = require('../models')
|
||||
|
||||
function getHistory (userid, callback) {
|
||||
models.User.findOne({
|
||||
|
@ -116,14 +116,14 @@ function parseHistoryToObject (history) {
|
|||
function historyGet (req, res) {
|
||||
if (req.isAuthenticated()) {
|
||||
getHistory(req.user.id, function (err, history) {
|
||||
if (err) return response.errorInternalError(res)
|
||||
if (!history) return response.errorNotFound(res)
|
||||
if (err) return response.errorInternalError(req, res)
|
||||
if (!history) return response.errorNotFound(req, res)
|
||||
res.send({
|
||||
history: parseHistoryToArray(history)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return response.errorForbidden(res)
|
||||
return response.errorForbidden(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,40 +131,40 @@ function historyPost (req, res) {
|
|||
if (req.isAuthenticated()) {
|
||||
var noteId = req.params.noteId
|
||||
if (!noteId) {
|
||||
if (typeof req.body['history'] === 'undefined') return response.errorBadRequest(res)
|
||||
if (typeof req.body['history'] === 'undefined') return response.errorBadRequest(req, res)
|
||||
if (config.debug) { logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history) }
|
||||
try {
|
||||
var history = JSON.parse(req.body.history)
|
||||
} catch (err) {
|
||||
return response.errorBadRequest(res)
|
||||
return response.errorBadRequest(req, res)
|
||||
}
|
||||
if (Array.isArray(history)) {
|
||||
setHistory(req.user.id, history, function (err, count) {
|
||||
if (err) return response.errorInternalError(res)
|
||||
if (err) return response.errorInternalError(req, res)
|
||||
res.end()
|
||||
})
|
||||
} else {
|
||||
return response.errorBadRequest(res)
|
||||
return response.errorBadRequest(req, res)
|
||||
}
|
||||
} else {
|
||||
if (typeof req.body['pinned'] === 'undefined') return response.errorBadRequest(res)
|
||||
if (typeof req.body['pinned'] === 'undefined') return response.errorBadRequest(req, res)
|
||||
getHistory(req.user.id, function (err, history) {
|
||||
if (err) return response.errorInternalError(res)
|
||||
if (!history) return response.errorNotFound(res)
|
||||
if (!history[noteId]) return response.errorNotFound(res)
|
||||
if (err) return response.errorInternalError(req, res)
|
||||
if (!history) return response.errorNotFound(req, res)
|
||||
if (!history[noteId]) return response.errorNotFound(req, res)
|
||||
if (req.body.pinned === 'true' || req.body.pinned === 'false') {
|
||||
history[noteId].pinned = (req.body.pinned === 'true')
|
||||
setHistory(req.user.id, history, function (err, count) {
|
||||
if (err) return response.errorInternalError(res)
|
||||
if (err) return response.errorInternalError(req, res)
|
||||
res.end()
|
||||
})
|
||||
} else {
|
||||
return response.errorBadRequest(res)
|
||||
return response.errorBadRequest(req, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return response.errorForbidden(res)
|
||||
return response.errorForbidden(req, res)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,22 +173,22 @@ function historyDelete (req, res) {
|
|||
var noteId = req.params.noteId
|
||||
if (!noteId) {
|
||||
setHistory(req.user.id, [], function (err, count) {
|
||||
if (err) return response.errorInternalError(res)
|
||||
if (err) return response.errorInternalError(req, res)
|
||||
res.end()
|
||||
})
|
||||
} else {
|
||||
getHistory(req.user.id, function (err, history) {
|
||||
if (err) return response.errorInternalError(res)
|
||||
if (!history) return response.errorNotFound(res)
|
||||
if (err) return response.errorInternalError(req, res)
|
||||
if (!history) return response.errorNotFound(req, res)
|
||||
delete history[noteId]
|
||||
setHistory(req.user.id, history, function (err, count) {
|
||||
if (err) return response.errorInternalError(res)
|
||||
if (err) return response.errorInternalError(req, res)
|
||||
res.end()
|
||||
})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
return response.errorForbidden(res)
|
||||
return response.errorForbidden(req, res)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const config = require('../config')
|
||||
const { User } = require('../models')
|
||||
const logger = require('../logger')
|
||||
|
||||
exports.showIndex = async (req, res) => {
|
||||
const isLogin = req.isAuthenticated()
|
||||
const deleteToken = ''
|
||||
|
||||
const data = {
|
||||
signin: isLogin,
|
||||
infoMessage: req.flash('info'),
|
||||
errorMessage: req.flash('error'),
|
||||
privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
|
||||
termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
|
||||
deleteToken: deleteToken
|
||||
}
|
||||
|
||||
if (!isLogin) {
|
||||
return res.render('index.ejs', data)
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
id: req.user.id
|
||||
}
|
||||
})
|
||||
if (user) {
|
||||
data.deleteToken = user.deleteToken
|
||||
return res.render('index.ejs', data)
|
||||
}
|
||||
|
||||
logger.error(`error: user not found with id ${req.user.id}`)
|
||||
return res.render('index.ejs', data)
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
'use strict'
|
||||
const path = require('path')
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
|
||||
const azure = require('azure-storage')
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
const URL = require('url').URL
|
||||
const path = require('path')
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
if (!imagePath || typeof imagePath !== 'string') {
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
|
||||
const imgur = require('@hackmd/imgur')
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
const Router = require('express').Router
|
||||
const formidable = require('formidable')
|
||||
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const response = require('../../response')
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
const response = require('../response')
|
||||
|
||||
const imageRouter = module.exports = Router()
|
||||
|
||||
|
@ -21,7 +21,7 @@ imageRouter.post('/uploadimage', function (req, res) {
|
|||
|
||||
form.parse(req, function (err, fields, files) {
|
||||
if (err || !files.image || !files.image.path) {
|
||||
response.errorForbidden(res)
|
||||
response.errorForbidden(req, res)
|
||||
} else {
|
||||
if (config.debug) {
|
||||
logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image))
|
|
@ -1,8 +1,8 @@
|
|||
'use strict'
|
||||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
|
||||
const lutim = require('lutim')
|
||||
const lutim = require('lib/imageRouter/lutim')
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
if (!imagePath || typeof imagePath !== 'string') {
|
|
@ -2,11 +2,11 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const config = require('../../config')
|
||||
const { getImageMimeType } = require('../../utils')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../config')
|
||||
const { getImageMimeType } = require('../utils')
|
||||
const logger = require('../logger')
|
||||
|
||||
const Minio = require('minio')
|
||||
const Minio = require('lib/imageRouter/minio')
|
||||
const minioClient = new Minio.Client({
|
||||
endPoint: config.minio.endPoint,
|
||||
port: config.minio.port,
|
|
@ -2,9 +2,9 @@
|
|||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const config = require('../../config')
|
||||
const { getImageMimeType } = require('../../utils')
|
||||
const logger = require('../../logger')
|
||||
const config = require('../config')
|
||||
const { getImageMimeType } = require('../utils')
|
||||
const logger = require('../logger')
|
||||
|
||||
const AWS = require('aws-sdk')
|
||||
const awsConfig = new AWS.Config(config.s3)
|
|
@ -1,14 +1,14 @@
|
|||
'use strict'
|
||||
|
||||
const logger = require('../../logger')
|
||||
const response = require('../../response')
|
||||
const logger = require('../logger')
|
||||
const response = require('../response')
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
try {
|
||||
decodeURIComponent(req.path)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return response.errorBadRequest(res)
|
||||
return response.errorBadRequest(req, res)
|
||||
}
|
||||
next()
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const config = require('../../config')
|
||||
const config = require('../config')
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
res.set({
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const config = require('../../config')
|
||||
const config = require('../config')
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) {
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
const toobusy = require('toobusy-js')
|
||||
|
||||
const config = require('../../config')
|
||||
const response = require('../../response')
|
||||
const config = require('../config')
|
||||
const response = require('../response')
|
||||
|
||||
toobusy.maxLag(config.responseMaxLag)
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
if (toobusy()) {
|
||||
response.errorServiceUnavailable(res)
|
||||
response.errorServiceUnavailable(req, res)
|
||||
} else {
|
||||
next()
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
return queryInterface.dropTable('Temp')
|
||||
/*
|
||||
Add altering commands here.
|
||||
Return a promise to correctly handle asynchronicity.
|
||||
|
||||
Example:
|
||||
return queryInterface.createTable('users', { id: Sequelize.INTEGER });
|
||||
*/
|
||||
},
|
||||
|
||||
down: (queryInterface, Sequelize) => {
|
||||
return queryInterface.createTable('Temp', {
|
||||
id: {
|
||||
type: Sequelize.STRING,
|
||||
primaryKey: true
|
||||
},
|
||||
date: Sequelize.TEXT,
|
||||
createdAt: Sequelize.DATE,
|
||||
updatedAt: Sequelize.DATE
|
||||
})
|
||||
}
|
||||
}
|
|
@ -186,6 +186,16 @@ module.exports = function (sequelize, DataTypes) {
|
|||
var result = id.match(uuidRegex)
|
||||
if (result && result.length === 1) { return true } else { return false }
|
||||
}
|
||||
Note.parseNoteIdAsync = function (noteId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Note.parseNoteId(noteId, (err, id) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
resolve(id)
|
||||
})
|
||||
})
|
||||
}
|
||||
Note.parseNoteId = function (noteId, callback) {
|
||||
async.series({
|
||||
parseNoteIdByAlias: function (_callback) {
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
'use strict'
|
||||
// external modules
|
||||
var shortId = require('shortid')
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
var Temp = sequelize.define('Temp', {
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
defaultValue: shortId.generate
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.TEXT
|
||||
}
|
||||
})
|
||||
|
||||
return Temp
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
'use strict'
|
||||
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
|
||||
const { Note, User } = require('../models')
|
||||
|
||||
const { newCheckViewPermission, errorForbidden, responseCodiMD, errorNotFound } = require('../response')
|
||||
const { updateHistory } = require('../history')
|
||||
const { actionPublish, actionSlide, actionInfo, actionDownload, actionPDF, actionGist, actionRevision, actionPandoc } = require('./noteActions')
|
||||
|
||||
async function getNoteById (noteId, { includeUser } = { includeUser: false }) {
|
||||
const id = await Note.parseNoteIdAsync(noteId)
|
||||
|
||||
const includes = []
|
||||
|
||||
if (includeUser) {
|
||||
includes.push({
|
||||
model: User,
|
||||
as: 'owner'
|
||||
}, {
|
||||
model: User,
|
||||
as: 'lastchangeuser'
|
||||
})
|
||||
}
|
||||
|
||||
const note = await Note.findOne({
|
||||
where: {
|
||||
id: id
|
||||
},
|
||||
include: includes
|
||||
})
|
||||
return note
|
||||
}
|
||||
|
||||
async function createNote (userId, noteAlias) {
|
||||
if (!config.allowAnonymous && !!userId) {
|
||||
throw new Error('can not create note')
|
||||
}
|
||||
|
||||
const note = await Note.create({
|
||||
ownerId: userId,
|
||||
alias: noteAlias
|
||||
})
|
||||
|
||||
if (userId) {
|
||||
updateHistory(userId, note)
|
||||
}
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
// controller
|
||||
async function showNote (req, res) {
|
||||
const noteId = req.params.noteId
|
||||
const userId = req.user ? req.user.id : null
|
||||
|
||||
let note = await getNoteById(noteId)
|
||||
|
||||
if (!note) {
|
||||
// if allow free url enable, auto create note
|
||||
if (!config.allowFreeURL || config.forbiddenNoteIDs.includes(noteId)) {
|
||||
return errorNotFound(req, res)
|
||||
}
|
||||
note = await createNote(userId, noteId)
|
||||
}
|
||||
|
||||
if (!newCheckViewPermission(note, req.isAuthenticated(), userId)) {
|
||||
return errorForbidden(req, res)
|
||||
}
|
||||
|
||||
// force to use note id
|
||||
const id = Note.encodeNoteId(note.id)
|
||||
if ((note.alias && noteId !== note.alias) || (!note.alias && noteId !== id)) {
|
||||
return res.redirect(config.serverURL + '/' + (note.alias || id))
|
||||
}
|
||||
return responseCodiMD(res, note)
|
||||
}
|
||||
|
||||
function canViewNote (note, isLogin, userId) {
|
||||
if (note.permission === 'private') {
|
||||
return note.ownerId === userId
|
||||
}
|
||||
if (note.permission === 'limited' || note.permission === 'protected') {
|
||||
return isLogin
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function showPublishNote (req, res) {
|
||||
const shortid = req.params.shortid
|
||||
|
||||
const note = await getNoteById(shortid, {
|
||||
includeUser: true
|
||||
})
|
||||
|
||||
if (!note) {
|
||||
return errorNotFound(req, res)
|
||||
}
|
||||
|
||||
if (!canViewNote(note, req.isAuthenticated(), req.user ? req.user.id : null)) {
|
||||
return errorForbidden(req, res)
|
||||
}
|
||||
|
||||
if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) {
|
||||
return res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid))
|
||||
}
|
||||
|
||||
await note.increment('viewcount')
|
||||
|
||||
const body = note.content
|
||||
const extracted = Note.extractMeta(body)
|
||||
const markdown = extracted.markdown
|
||||
const meta = Note.parseMeta(extracted.meta)
|
||||
const createTime = note.createdAt
|
||||
const updateTime = note.lastchangeAt
|
||||
const title = Note.generateWebTitle(meta.title || Note.decodeTitle(note.title))
|
||||
|
||||
const data = {
|
||||
title: title,
|
||||
description: meta.description || (markdown ? Note.generateDescription(markdown) : null),
|
||||
viewcount: note.viewcount,
|
||||
createtime: createTime,
|
||||
updatetime: updateTime,
|
||||
body: body,
|
||||
owner: note.owner ? note.owner.id : null,
|
||||
ownerprofile: note.owner ? User.getProfile(note.owner) : null,
|
||||
lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null,
|
||||
lastchangeuserprofile: note.lastchangeuser ? User.getProfile(note.lastchangeuser) : null,
|
||||
robots: meta.robots || false, // default allow robots
|
||||
GA: meta.GA,
|
||||
disqus: meta.disqus,
|
||||
cspNonce: res.locals.nonce
|
||||
}
|
||||
|
||||
res.set({
|
||||
'Cache-Control': 'private' // only cache by client
|
||||
})
|
||||
|
||||
res.render('pretty.ejs', data)
|
||||
}
|
||||
|
||||
async function noteActions (req, res) {
|
||||
const noteId = req.params.noteId
|
||||
|
||||
const note = await getNoteById(noteId)
|
||||
|
||||
if (!note) {
|
||||
return errorNotFound(req, res)
|
||||
}
|
||||
|
||||
if (!canViewNote(note, req.isAuthenticated(), req.user ? req.user.id : null)) {
|
||||
return errorForbidden(req, res)
|
||||
}
|
||||
|
||||
const action = req.params.action
|
||||
switch (action) {
|
||||
case 'publish':
|
||||
case 'pretty': // pretty deprecated
|
||||
return actionPublish(req, res, note)
|
||||
case 'slide':
|
||||
return actionSlide(req, res, note)
|
||||
case 'download':
|
||||
actionDownload(req, res, note)
|
||||
break
|
||||
case 'info':
|
||||
actionInfo(req, res, note)
|
||||
break
|
||||
case 'pdf':
|
||||
if (config.allowPDFExport) {
|
||||
actionPDF(req, res, note)
|
||||
} else {
|
||||
logger.error('PDF export failed: Disabled by config. Set "allowPDFExport: true" to enable. Check the documentation for details')
|
||||
errorForbidden(req, res)
|
||||
}
|
||||
break
|
||||
case 'gist':
|
||||
actionGist(req, res, note)
|
||||
break
|
||||
case 'revision':
|
||||
actionRevision(req, res, note)
|
||||
break
|
||||
case 'pandoc':
|
||||
actionPandoc(req, res, note)
|
||||
break
|
||||
default:
|
||||
return res.redirect(config.serverURL + '/' + noteId)
|
||||
}
|
||||
}
|
||||
|
||||
exports.showNote = showNote
|
||||
exports.showPublishNote = showPublishNote
|
||||
exports.noteActions = noteActions
|
|
@ -0,0 +1,226 @@
|
|||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const markdownpdf = require('markdown-pdf')
|
||||
const shortId = require('shortid')
|
||||
const querystring = require('querystring')
|
||||
const moment = require('moment')
|
||||
const { Pandoc } = require('@hackmd/pandoc.js')
|
||||
|
||||
const config = require('../config')
|
||||
const logger = require('../logger')
|
||||
const { Note, Revision } = require('../models')
|
||||
const { errorInternalError, errorNotFound } = require('../response')
|
||||
|
||||
function actionPublish (req, res, note) {
|
||||
res.redirect(config.serverURL + '/s/' + (note.alias || note.shortid))
|
||||
}
|
||||
|
||||
function actionSlide (req, res, note) {
|
||||
res.redirect(config.serverURL + '/p/' + (note.alias || note.shortid))
|
||||
}
|
||||
|
||||
function actionDownload (req, res, note) {
|
||||
const body = note.content
|
||||
const title = Note.decodeTitle(note.title)
|
||||
const filename = encodeURIComponent(title)
|
||||
res.set({
|
||||
'Access-Control-Allow-Origin': '*', // allow CORS as API
|
||||
'Access-Control-Allow-Headers': 'Range',
|
||||
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
|
||||
'Content-Type': 'text/markdown; charset=UTF-8',
|
||||
'Cache-Control': 'private',
|
||||
'Content-disposition': 'attachment; filename=' + filename + '.md',
|
||||
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
|
||||
})
|
||||
res.send(body)
|
||||
}
|
||||
|
||||
function actionInfo (req, res, note) {
|
||||
const body = note.content
|
||||
const extracted = Note.extractMeta(body)
|
||||
const markdown = extracted.markdown
|
||||
const meta = Note.parseMeta(extracted.meta)
|
||||
const createtime = note.createdAt
|
||||
const updatetime = note.lastchangeAt
|
||||
const title = Note.decodeTitle(note.title)
|
||||
|
||||
const data = {
|
||||
title: meta.title || title,
|
||||
description: meta.description || (markdown ? Note.generateDescription(markdown) : null),
|
||||
viewcount: note.viewcount,
|
||||
createtime: createtime,
|
||||
updatetime: updatetime
|
||||
}
|
||||
|
||||
res.set({
|
||||
'Access-Control-Allow-Origin': '*', // allow CORS as API
|
||||
'Access-Control-Allow-Headers': 'Range',
|
||||
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
|
||||
'Cache-Control': 'private', // only cache by client
|
||||
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
|
||||
})
|
||||
res.send(data)
|
||||
}
|
||||
|
||||
function actionPDF (req, res, note) {
|
||||
const url = config.serverURL || 'http://' + req.get('host')
|
||||
const body = note.content
|
||||
const extracted = Note.extractMeta(body)
|
||||
let content = extracted.markdown
|
||||
const title = Note.decodeTitle(note.title)
|
||||
|
||||
const highlightCssPath = path.join(config.appRootPath, '/node_modules/highlight.js/styles/github-gist.css')
|
||||
|
||||
if (!fs.existsSync(config.tmpPath)) {
|
||||
fs.mkdirSync(config.tmpPath)
|
||||
}
|
||||
const pdfPath = config.tmpPath + '/' + Date.now() + '.pdf'
|
||||
content = content.replace(/\]\(\//g, '](' + url + '/')
|
||||
const markdownpdfOptions = {
|
||||
highlightCssPath: highlightCssPath
|
||||
}
|
||||
markdownpdf(markdownpdfOptions).from.string(content).to(pdfPath, function () {
|
||||
if (!fs.existsSync(pdfPath)) {
|
||||
logger.error('PDF seems to not be generated as expected. File doesn\'t exist: ' + pdfPath)
|
||||
return errorInternalError(req, res)
|
||||
}
|
||||
const stream = fs.createReadStream(pdfPath)
|
||||
let filename = title
|
||||
// Be careful of special characters
|
||||
filename = encodeURIComponent(filename)
|
||||
// Ideally this should strip them
|
||||
res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"')
|
||||
res.setHeader('Cache-Control', 'private')
|
||||
res.setHeader('Content-Type', 'application/pdf; charset=UTF-8')
|
||||
res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling
|
||||
stream.on('end', () => {
|
||||
stream.close()
|
||||
fs.unlinkSync(pdfPath)
|
||||
})
|
||||
stream.pipe(res)
|
||||
})
|
||||
}
|
||||
|
||||
const outputFormats = {
|
||||
asciidoc: 'text/plain',
|
||||
context: 'application/x-latex',
|
||||
epub: 'application/epub+zip',
|
||||
epub3: 'application/epub+zip',
|
||||
latex: 'application/x-latex',
|
||||
odt: 'application/vnd.oasis.opendocument.text',
|
||||
pdf: 'application/pdf',
|
||||
rst: 'text/plain',
|
||||
rtf: 'application/rtf',
|
||||
textile: 'text/plain',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
}
|
||||
|
||||
async function actionPandoc (req, res, note) {
|
||||
var url = config.serverURL || 'http://' + req.get('host')
|
||||
var body = note.content
|
||||
var extracted = Note.extractMeta(body)
|
||||
var content = extracted.markdown
|
||||
var title = Note.decodeTitle(note.title)
|
||||
|
||||
if (!fs.existsSync(config.tmpPath)) {
|
||||
fs.mkdirSync(config.tmpPath)
|
||||
}
|
||||
const pandoc = new Pandoc()
|
||||
|
||||
var path = config.tmpPath + '/' + Date.now()
|
||||
content = content.replace(/\]\(\//g, '](' + url + '/')
|
||||
|
||||
// TODO: check export type
|
||||
const { exportType } = req.query
|
||||
|
||||
try {
|
||||
// TODO: timeout rejection
|
||||
|
||||
await pandoc.convertToFile(content, 'markdown', exportType, path, [
|
||||
'--metadata', `title=${title}`
|
||||
])
|
||||
|
||||
var stream = fs.createReadStream(path)
|
||||
var filename = title
|
||||
// Be careful of special characters
|
||||
filename = encodeURIComponent(filename)
|
||||
// Ideally this should strip them
|
||||
res.setHeader('Content-disposition', `attachment; filename="${filename}.${exportType}"`)
|
||||
res.setHeader('Cache-Control', 'private')
|
||||
res.setHeader('Content-Type', `${outputFormats[exportType]}; charset=UTF-8`)
|
||||
res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling
|
||||
stream.pipe(res)
|
||||
} catch (err) {
|
||||
// TODO: handle error
|
||||
res.json({
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function actionGist (req, res, note) {
|
||||
const data = {
|
||||
client_id: config.github.clientID,
|
||||
redirect_uri: config.serverURL + '/auth/github/callback/' + Note.encodeNoteId(note.id) + '/gist',
|
||||
scope: 'gist',
|
||||
state: shortId.generate()
|
||||
}
|
||||
const query = querystring.stringify(data)
|
||||
res.redirect('https://github.com/login/oauth/authorize?' + query)
|
||||
}
|
||||
|
||||
function actionRevision (req, res, note) {
|
||||
const actionId = req.params.actionId
|
||||
if (actionId) {
|
||||
const time = moment(parseInt(actionId))
|
||||
if (!time.isValid()) {
|
||||
return errorNotFound(req, res)
|
||||
}
|
||||
Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return errorInternalError(req, res)
|
||||
}
|
||||
if (!content) {
|
||||
return errorNotFound(req, res)
|
||||
}
|
||||
res.set({
|
||||
'Access-Control-Allow-Origin': '*', // allow CORS as API
|
||||
'Access-Control-Allow-Headers': 'Range',
|
||||
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
|
||||
'Cache-Control': 'private', // only cache by client
|
||||
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
|
||||
})
|
||||
res.send(content)
|
||||
})
|
||||
} else {
|
||||
Revision.getNoteRevisions(note, function (err, data) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return errorInternalError(req, res)
|
||||
}
|
||||
const result = {
|
||||
revision: data
|
||||
}
|
||||
res.set({
|
||||
'Access-Control-Allow-Origin': '*', // allow CORS as API
|
||||
'Access-Control-Allow-Headers': 'Range',
|
||||
'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range',
|
||||
'Cache-Control': 'private', // only cache by client
|
||||
'X-Robots-Tag': 'noindex, nofollow' // prevent crawling
|
||||
})
|
||||
res.send(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.actionPublish = actionPublish
|
||||
exports.actionSlide = actionSlide
|
||||
exports.actionDownload = actionDownload
|
||||
exports.actionInfo = actionInfo
|
||||
exports.actionPDF = actionPDF
|
||||
exports.actionGist = actionGist
|
||||
exports.actionPandoc = actionPandoc
|
||||
exports.actionRevision = actionRevision
|