Merge branch 'master' into DepauMD
This commit is contained in:
commit
d59212ea8b
82 changed files with 2240 additions and 2053 deletions
2
.babelrc
2
.babelrc
|
@ -2,7 +2,7 @@
|
|||
"presets": [
|
||||
["env", {
|
||||
"targets": {
|
||||
"node": "6",
|
||||
"node": "8",
|
||||
"uglify": true
|
||||
}
|
||||
}]
|
||||
|
|
|
@ -10,6 +10,7 @@ module.exports = {
|
|||
// wrong.
|
||||
"import/first": ["warn"],
|
||||
"indent": ["warn"],
|
||||
"no-console": ["warn"],
|
||||
"no-multiple-empty-lines": ["warn"],
|
||||
"no-multi-spaces": ["warn"],
|
||||
"object-curly-spacing": ["warn"],
|
||||
|
|
48
.travis.yml
48
.travis.yml
|
@ -1,40 +1,40 @@
|
|||
language: node_js
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
cache: yarn
|
||||
env:
|
||||
global:
|
||||
- CXX=g++-4.8
|
||||
- YARN_VERSION=1.15.2
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- env: task=npm-test
|
||||
node_js:
|
||||
- 6
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- env: task=npm-test
|
||||
node_js:
|
||||
- 8
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- env: task=npm-test
|
||||
- stage: Static Tests
|
||||
name: eslint
|
||||
node_js:
|
||||
- 10
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- env: task=ShellCheck
|
||||
script:
|
||||
- yarn run eslint
|
||||
- name: ShellCheck
|
||||
script:
|
||||
- shellcheck bin/heroku bin/setup
|
||||
language: generic
|
||||
- env: task=json-lint
|
||||
- name: json-lint
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- jq
|
||||
script:
|
||||
- npm run jsonlint
|
||||
- yarn run jsonlint
|
||||
language: generic
|
||||
- stage: Dynamic Tests
|
||||
name: Node.js 8
|
||||
node_js:
|
||||
- 8
|
||||
script:
|
||||
- yarn run mocha-suite
|
||||
- name: Node.js 10
|
||||
node_js:
|
||||
- 10
|
||||
script:
|
||||
- yarn run mocha-suite
|
||||
- name: Node.js 12
|
||||
node_js:
|
||||
- 12
|
||||
script:
|
||||
- yarn run mocha-suite
|
||||
|
|
|
@ -5,6 +5,7 @@ CodiMD
|
|||
[![build status][travis-image]][travis-url]
|
||||
[![version][github-version-badge]][github-release-page]
|
||||
[![POEditor][poeditor-image]][poeditor-url]
|
||||
[![Mastodon][social-mastodon-image]][social-mastodon]
|
||||
|
||||
CodiMD lets you create real-time collaborative markdown notes. You can test-drive
|
||||
it by visiting our [CodiMD demo server][codimd-demo].
|
||||
|
@ -98,3 +99,5 @@ Licensed under AGPLv3. For our list of contributors, see [AUTHORS](AUTHORS).
|
|||
[codimd-demo-features]: https://demo.codimd.org/features
|
||||
[codimd-community]: https://community.codimd.org
|
||||
[codimd-community-calls]: https://community.codimd.org/t/codimd-community-call/19
|
||||
[social-mastodon]: https://social.codimd.org/mastodon
|
||||
[social-mastodon-image]: https://img.shields.io/badge/social-mastodon-3c99dc.svg
|
||||
|
|
2
app.js
2
app.js
|
@ -113,7 +113,7 @@ if (config.csp.enable) {
|
|||
}
|
||||
|
||||
i18n.configure({
|
||||
locales: ['en', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko', 'id', 'sr'],
|
||||
locales: ['en', 'zh-CN', 'zh-TW', 'fr', 'de', 'ja', 'es', 'ca', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo', 'da', 'ko', 'id', 'sr', 'vi'],
|
||||
cookie: 'locale',
|
||||
indent: ' ', // this is the style poeditor.com exports it, this creates less churn
|
||||
directory: path.join(__dirname, '/locales'),
|
||||
|
|
|
@ -32,6 +32,7 @@ to `config.json` before filling in your own details.
|
|||
| `imageUploadType` | `imgur`, `s3`, `minio`, `azure`, `lutim` or `filesystem`(default) | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) or [Minio](guides/minio-image-upload.md)|
|
||||
| `sourceURL` | `https://github.com/codimd/server/tree/<current commit>` | Provides the link to the source code of CodiMD on the entry page (Please, make sure you change this when you run a modified version) |
|
||||
| `staticCacheTime` | `1 * 24 * 60 * 60 * 1000` | static file cache time |
|
||||
| `tooBusyLag` | `70` | CPU time for one eventloop tick until node throttles connections. (milliseconds) |
|
||||
| `heartbeatInterval` | `5000` | socket.io heartbeat interval |
|
||||
| `heartbeatTimeout` | `10000` | socket.io heartbeat timeout |
|
||||
| `documentMaxLength` | `100000` | note max length |
|
||||
|
|
|
@ -35,6 +35,7 @@ defaultNotePath can't be set from env-vars
|
|||
| `CMD_FORBIDDEN_NOTE_IDS` | `'robots.txt'` | disallow creation of notes, even if `CMD_ALLOW_FREEURL` is `true` |
|
||||
| `CMD_IMAGE_UPLOAD_TYPE` | `imgur`, `s3`, `minio`, `lutim` or `filesystem` | Where to upload images. For S3, see our Image Upload Guides for [S3](guides/s3-image-upload.md) or [Minio](guides/minio-image-upload.md), also there's a whole section on their respective env vars below. |
|
||||
| `CMD_SOURCE_URL` | `https://github.com/codimd/server/tree/<current commit>` | Provides the link to the source code of CodiMD on the entry page (Please, make sure you change this when you run a modified version) |
|
||||
| `CMD_TOOBUSY_LAG` | `70` | CPU time for one eventloop tick until node throttles connections. (milliseconds) |
|
||||
|
||||
|
||||
## CodiMD Location
|
||||
|
|
50
docs/guides/auth/keycloak.md
Normal file
50
docs/guides/auth/keycloak.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
Keycloak/Red Hat SSO (self-hosted)
|
||||
===
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This guide assumes you have run and configured Keycloak. If you'd like to meet this prerequisite quickly, it can be achieved by running a `jboss/keycloak` container and attaching it to your network. Set the environment variables KEYCLOAK_USER and `KEYCLOAK_PASSWORD`, and expose port 8080.
|
||||
|
||||
Where HTTPS is specified throughout, use HTTP instead. You may also have to specify the exposed port, 8080.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Sign in to the administration portal for your Keycloak instance at https://keycloak.example.com/auth/admin/master/console
|
||||
|
||||
You may note that a separate realm is specified throughout this tutorial. It is best practice not to use the master realm, as it normally contains the realm-management client that federates access using the policies and permissions you can create.
|
||||
|
||||
2. Navigate to the client management page at `https://keycloak.example.com/auth/admin/master/console/#/realms/your-realm/clients` (admin permissions required)
|
||||
3. Click **Create** to create a new client and fill out the registration form. You should set the Root URL to the fully qualified public URL of your CodiMD instance.
|
||||
4. Click **Save**
|
||||
5. Set the **Access Type** of the client to `confidential`. This will make your client require a client secret upon authentication.
|
||||
|
||||
---
|
||||
|
||||
### Additional steps to circumvent generic OAuth2 issue:
|
||||
|
||||
1. Select Client Scopes from the sidebar, and begin to create a new client scope using the Create button.
|
||||
2. Ensure that the **Name** field is set to `id`.
|
||||
3. Create a new mapper under the Mappers tab. This should reference the User Property `id`. `Claim JSON Type` should be String and all switches below should be enabled. Save the mapper.
|
||||
4. Go to the client you set up in the previous steps using the Clients page, then choose the Client Scopes tab. Apply the scope you've created. This should mitigate errors as seen in [codimd/server#56](https://github.com/codimd/server/issues/56), as the `/userinfo` endpoint should now bring back the user's ID under the `id` key as well as `sub`.
|
||||
|
||||
---
|
||||
|
||||
6. In the `docker-compose.yml` add the following environment variables to `app:` `environment:`
|
||||
|
||||
```
|
||||
CMD_OAUTH2_USER_PROFILE_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/userinfo
|
||||
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR=preferred_username
|
||||
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR=name
|
||||
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR=email
|
||||
CMD_OAUTH2_TOKEN_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/token
|
||||
CMD_OAUTH2_AUTHORIZATION_URL=https://keycloak.example.com/auth/realms/your-realm/protocol/openid-connect/auth
|
||||
CMD_OAUTH2_CLIENT_ID=<your client ID>
|
||||
CMD_OAUTH2_CLIENT_SECRET=<your client secret, which you can find under the Credentials tab for your client>
|
||||
CMD_OAUTH2_PROVIDERNAME=Keycloak
|
||||
CMD_DOMAIN=<codimd.example.com>
|
||||
CMD_PROTOCOL_USESSL=true
|
||||
CMD_URL_ADDPORT=false
|
||||
```
|
||||
|
||||
7. Run `docker-compose up -d` to apply your settings.
|
||||
8. Sign in to your CodiMD using your Keycloak ID
|
|
@ -1,6 +1,10 @@
|
|||
Migrations and Notable Changes
|
||||
===
|
||||
|
||||
## Migrating to 1.4.0
|
||||
|
||||
We dropped support for node 6 with this version. If you have any trouble running this version, please double check that you are running at least node 8!
|
||||
|
||||
## Migrating to 1.3.2
|
||||
|
||||
This is not a breaking change, but to stay up to date with the community
|
||||
|
|
|
@ -16,7 +16,7 @@ CodiMD by docker container
|
|||
The easiest way to setup CodiMD using docker are using the following three commands:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/codimd/container.git
|
||||
git clone https://github.com/codimd/container.git codimd-container
|
||||
cd codimd-container
|
||||
docker-compose up
|
||||
```
|
||||
|
|
|
@ -3,11 +3,10 @@ Manual Installation
|
|||
|
||||
## Requirements on your server
|
||||
|
||||
- Node.js 6.x or up (test up to 7.5.0) and <10.x
|
||||
- Node.js 8.5 or up
|
||||
- Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) use charset `utf8`
|
||||
- npm (and its dependencies, [node-gyp](https://github.com/nodejs/node-gyp#installation))
|
||||
- yarn
|
||||
- `libssl-dev` for building scrypt (see [here](https://github.com/ml1nk/node-scrypt/blob/master/README.md#installation-instructions) for further information)
|
||||
- Bash (for the setup script)
|
||||
- For **building** CodiMD we recommend to use a machine with at least **2GB** RAM
|
||||
|
||||
|
|
|
@ -56,6 +56,8 @@ module.exports = {
|
|||
// socket.io
|
||||
heartbeatInterval: 5000,
|
||||
heartbeatTimeout: 10000,
|
||||
// too busy timeout
|
||||
tooBusyLag: 70,
|
||||
// document
|
||||
documentMaxLength: 100000,
|
||||
// image upload setting, available options are imgur/s3/filesystem/azure/lutim
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')
|
||||
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
|
||||
|
||||
module.exports = {
|
||||
sourceURL: process.env.CMD_SOURCE_URL,
|
||||
|
@ -33,6 +33,7 @@ module.exports = {
|
|||
dbURL: process.env.CMD_DB_URL,
|
||||
sessionSecret: process.env.CMD_SESSION_SECRET,
|
||||
sessionLife: toIntegerConfig(process.env.CMD_SESSION_LIFE),
|
||||
tooBusyLag: toIntegerConfig(process.env.CMD_TOOBUSY_LAG),
|
||||
imageUploadType: process.env.CMD_IMAGE_UPLOAD_TYPE,
|
||||
imgur: {
|
||||
clientID: process.env.CMD_IMGUR_CLIENTID
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')
|
||||
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
|
||||
|
||||
module.exports = {
|
||||
domain: process.env.HMD_DOMAIN,
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const {merge} = require('lodash')
|
||||
const { merge } = require('lodash')
|
||||
const deepFreeze = require('deep-freeze')
|
||||
const {Environment, Permission} = require('./enum')
|
||||
const { Environment, Permission } = require('./enum')
|
||||
const logger = require('../logger')
|
||||
const {getGitCommit, getGitHubURL} = require('./utils')
|
||||
const { getGitCommit, getGitHubURL } = require('./utils')
|
||||
|
||||
const appRootPath = path.resolve(__dirname, '../../')
|
||||
const env = process.env.NODE_ENV || Environment.development
|
||||
|
@ -17,7 +17,7 @@ const debugConfig = {
|
|||
}
|
||||
|
||||
// Get version string from package.json
|
||||
const {version, repository} = require(path.join(appRootPath, 'package.json'))
|
||||
const { version, repository } = require(path.join(appRootPath, 'package.json'))
|
||||
|
||||
const commitID = getGitCommit(appRootPath)
|
||||
const sourceURL = getGitHubURL(repository.url, commitID || version)
|
||||
|
@ -159,8 +159,8 @@ if (Object.keys(process.env).toString().indexOf('HMD_') !== -1) {
|
|||
if (config.sessionSecret === 'secret') {
|
||||
logger.warn('Session secret not set. Using random generated one. Please set `sessionSecret` in your config.js file. All users will be logged out.')
|
||||
config.sessionSecret = crypto.randomBytes(Math.ceil(config.sessionSecretLen / 2)) // generate crypto graphic random number
|
||||
.toString('hex') // convert to hexadecimal format
|
||||
.slice(0, config.sessionSecretLen) // return required number of characters
|
||||
.toString('hex') // convert to hexadecimal format
|
||||
.slice(0, config.sessionSecretLen) // return required number of characters
|
||||
}
|
||||
|
||||
// Validate upload upload providers
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const {toBooleanConfig} = require('./utils')
|
||||
const { toBooleanConfig } = require('./utils')
|
||||
|
||||
module.exports = {
|
||||
debug: toBooleanConfig(process.env.DEBUG),
|
||||
|
|
|
@ -30,14 +30,14 @@ exports.generateAvatarURL = function (name, email = '', big = true) {
|
|||
if (typeof email !== 'string') {
|
||||
email = '' + name + '@example.com'
|
||||
}
|
||||
name=encodeURIComponent(name)
|
||||
name = encodeURIComponent(name)
|
||||
|
||||
let hash = crypto.createHash('md5')
|
||||
hash.update(email.toLowerCase())
|
||||
let hexDigest = hash.digest('hex')
|
||||
|
||||
if (email !== '' && config.allowGravatar) {
|
||||
photo = 'https://cdn.libravatar.org/avatar/' + hexDigest;
|
||||
photo = 'https://cdn.libravatar.org/avatar/' + hexDigest
|
||||
if (big) {
|
||||
photo += '?s=400'
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
'use strict'
|
||||
const {createLogger, format, transports} = require('winston')
|
||||
const { createLogger, format, transports } = require('winston')
|
||||
|
||||
const logger = createLogger({
|
||||
level: 'debug',
|
||||
|
|
|
@ -22,6 +22,7 @@ module.exports = {
|
|||
})
|
||||
}).catch(function (error) {
|
||||
if (error.message === 'SQLITE_ERROR: duplicate column name: shortid' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'shortid'" || error.message === 'column "shortid" of relation "Notes" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
|
|
@ -9,6 +9,7 @@ module.exports = {
|
|||
})
|
||||
}).catch(function (error) {
|
||||
if (error.message === 'SQLITE_ERROR: duplicate column name: lastchangeuserId' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'lastchangeuserId'" || error.message === 'column "lastchangeuserId" of relation "Notes" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
@ -18,8 +19,8 @@ module.exports = {
|
|||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
return queryInterface.removeColumn('Notes', 'lastchangeAt')
|
||||
.then(function () {
|
||||
return queryInterface.removeColumn('Notes', 'lastchangeuserId')
|
||||
})
|
||||
.then(function () {
|
||||
return queryInterface.removeColumn('Notes', 'lastchangeuserId')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ module.exports = {
|
|||
})
|
||||
}).catch(function (error) {
|
||||
if (error.message === 'SQLITE_ERROR: duplicate column name: alias' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'alias'" || error.message === 'column "alias" of relation "Notes" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
|
|
@ -5,6 +5,7 @@ module.exports = {
|
|||
return queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING)
|
||||
}).catch(function (error) {
|
||||
if (error.message === 'SQLITE_ERROR: duplicate column name: accessToken' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'accessToken'" || error.message === 'column "accessToken" of relation "Users" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
|
|
@ -17,6 +17,7 @@ module.exports = {
|
|||
})
|
||||
}).catch(function (error) {
|
||||
if (error.message === 'SQLITE_ERROR: duplicate column name: savedAt' | error.message === "ER_DUP_FIELDNAME: Duplicate column name 'savedAt'" || error.message === 'column "savedAt" of relation "Notes" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
|
|
@ -18,6 +18,7 @@ module.exports = {
|
|||
})
|
||||
}).catch(function (error) {
|
||||
if (error.message === 'SQLITE_ERROR: duplicate column name: authorship' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'authorship'" || error.message === 'column "authorship" of relation "Notes" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
|
|
@ -3,6 +3,7 @@ module.exports = {
|
|||
up: function (queryInterface, Sequelize) {
|
||||
return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE).catch(function (error) {
|
||||
if (error.message === 'SQLITE_ERROR: duplicate column name: deletedAt' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'deletedAt'" || error.message === 'column "deletedAt" of relation "Notes" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
|
|
@ -4,6 +4,7 @@ module.exports = {
|
|||
return queryInterface.addColumn('Users', 'email', Sequelize.TEXT).then(function () {
|
||||
return queryInterface.addColumn('Users', 'password', Sequelize.TEXT).catch(function (error) {
|
||||
if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'password'" || error.message === 'column "password" of relation "Users" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
@ -11,6 +12,7 @@ module.exports = {
|
|||
})
|
||||
}).catch(function (error) {
|
||||
if (error.message === 'SQLITE_ERROR: duplicate column name: email' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'email'" || error.message === 'column "email" of relation "Users" already exists') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Migration has already run… ignoring.')
|
||||
} else {
|
||||
throw error
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
'use strict'
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Revisions', 'lastContent', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') })
|
||||
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') })
|
||||
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') })
|
||||
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') })
|
||||
},
|
||||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Revisions', 'lastContent', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT })
|
||||
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT })
|
||||
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT })
|
||||
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'authorship', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Revisions', 'authorship', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') })
|
||||
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') })
|
||||
},
|
||||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'authorship', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Revisions', 'authorship', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT })
|
||||
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'permission', {type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private')})
|
||||
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') })
|
||||
},
|
||||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'permission', {type: Sequelize.ENUM('freely', 'editable', 'locked', 'private')})
|
||||
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
var Sequelize = require('sequelize')
|
||||
const {cloneDeep} = require('lodash')
|
||||
const { cloneDeep } = require('lodash')
|
||||
|
||||
// core
|
||||
var config = require('../config')
|
||||
|
@ -39,13 +39,13 @@ sequelize.processData = processData
|
|||
var db = {}
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter(function (file) {
|
||||
return (file.indexOf('.') !== 0) && (file !== 'index.js')
|
||||
})
|
||||
.forEach(function (file) {
|
||||
var model = sequelize.import(path.join(__dirname, file))
|
||||
db[model.name] = model
|
||||
})
|
||||
.filter(function (file) {
|
||||
return (file.indexOf('.') !== 0) && (file !== 'index.js')
|
||||
})
|
||||
.forEach(function (file) {
|
||||
var model = sequelize.import(path.join(__dirname, file))
|
||||
db[model.name] = model
|
||||
})
|
||||
|
||||
Object.keys(db).forEach(function (modelName) {
|
||||
if ('associate' in db[modelName]) {
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
'use strict'
|
||||
// external modules
|
||||
var Sequelize = require('sequelize')
|
||||
var scrypt = require('scrypt')
|
||||
const Sequelize = require('sequelize')
|
||||
const crypto = require('crypto')
|
||||
if (!crypto.scrypt) {
|
||||
// polyfill for node.js 8.0, see https://github.com/chrisveness/scrypt-kdf#openssl-implementation
|
||||
const scryptAsync = require('scrypt-async')
|
||||
crypto.scrypt = function (password, salt, keylen, options, callback) {
|
||||
const opt = Object.assign({}, options, { dkLen: keylen })
|
||||
scryptAsync(password, salt, opt, (derivedKey) => callback(null, Buffer.from(derivedKey)))
|
||||
}
|
||||
}
|
||||
const scrypt = require('scrypt-kdf')
|
||||
|
||||
// core
|
||||
var logger = require('../logger')
|
||||
var {generateAvatarURL} = require('../letter-avatars')
|
||||
const logger = require('../logger')
|
||||
const { generateAvatarURL } = require('../letter-avatars')
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
var User = sequelize.define('User', {
|
||||
|
@ -41,20 +50,12 @@ module.exports = function (sequelize, DataTypes) {
|
|||
}
|
||||
},
|
||||
password: {
|
||||
type: Sequelize.TEXT,
|
||||
set: function (value) {
|
||||
var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString('hex')
|
||||
this.setDataValue('password', hash)
|
||||
}
|
||||
type: Sequelize.TEXT
|
||||
}
|
||||
}, {
|
||||
instanceMethods: {
|
||||
verifyPassword: function (attempt) {
|
||||
if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) {
|
||||
return this
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return scrypt.verify(Buffer.from(this.password, 'hex'), attempt)
|
||||
}
|
||||
},
|
||||
classMethods: {
|
||||
|
@ -140,6 +141,9 @@ module.exports = function (sequelize, DataTypes) {
|
|||
case 'saml':
|
||||
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
|
||||
break
|
||||
default:
|
||||
photo = generateAvatarURL(profile.username)
|
||||
break
|
||||
}
|
||||
return photo
|
||||
},
|
||||
|
@ -153,5 +157,19 @@ module.exports = function (sequelize, DataTypes) {
|
|||
}
|
||||
})
|
||||
|
||||
function updatePasswordHashHook (user, options, done) {
|
||||
// suggested way to hash passwords to be able to do this asynchronously:
|
||||
// @see https://github.com/sequelize/sequelize/issues/1821#issuecomment-44265819
|
||||
if (!user.changed('password')) { return done() }
|
||||
|
||||
scrypt.kdf(user.getDataValue('password'), { logN: 15 }).then(keyBuf => {
|
||||
user.setDataValue('password', keyBuf.toString('hex'))
|
||||
done()
|
||||
})
|
||||
}
|
||||
|
||||
User.beforeCreate(updatePasswordHashHook)
|
||||
User.beforeUpdate(updatePasswordHashHook)
|
||||
|
||||
return User
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ var utils = require('./utils')
|
|||
// public
|
||||
var response = {
|
||||
errorForbidden: function (res) {
|
||||
const {req} = res
|
||||
const { req } = res
|
||||
if (req.user) {
|
||||
responseError(res, '403', 'Forbidden', 'oh no.')
|
||||
} else {
|
||||
|
@ -549,16 +549,16 @@ function gitlabActionProjects (req, res, note) {
|
|||
ret.accesstoken = user.accessToken
|
||||
ret.profileid = user.profileid
|
||||
request(
|
||||
config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken,
|
||||
function (error, httpResponse, body) {
|
||||
if (!error && httpResponse.statusCode === 200) {
|
||||
ret.projects = JSON.parse(body)
|
||||
return res.send(ret)
|
||||
} else {
|
||||
return res.send(ret)
|
||||
}
|
||||
}
|
||||
)
|
||||
config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken,
|
||||
function (error, httpResponse, body) {
|
||||
if (!error && httpResponse.statusCode === 200) {
|
||||
ret.projects = JSON.parse(body)
|
||||
return res.send(ret)
|
||||
} else {
|
||||
return res.send(ret)
|
||||
}
|
||||
}
|
||||
)
|
||||
}).catch(function (err) {
|
||||
logger.error('gitlab action projects failed: ' + err)
|
||||
return response.errorInternalError(res)
|
||||
|
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let dropboxAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ const LocalStrategy = require('passport-local').Strategy
|
|||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const {setReturnToFromReferer} = require('../utils')
|
||||
const {urlencodedParser} = require('../../utils')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const response = require('../../../response')
|
||||
|
||||
let emailAuth = module.exports = Router()
|
||||
|
@ -23,8 +23,14 @@ passport.use(new LocalStrategy({
|
|||
}
|
||||
}).then(function (user) {
|
||||
if (!user) return done(null, false)
|
||||
if (!user.verifyPassword(password)) return done(null, false)
|
||||
return done(null, user)
|
||||
user.verifyPassword(password).then(verified => {
|
||||
if (verified) {
|
||||
return done(null, user)
|
||||
} else {
|
||||
logger.warn('invalid password given for %s', user.email)
|
||||
return done(null, false)
|
||||
}
|
||||
})
|
||||
}).catch(function (err) {
|
||||
logger.error(err)
|
||||
return done(err)
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const FacebookStrategy = require('passport-facebook').Strategy
|
||||
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let facebookAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const GithubStrategy = require('passport-github').Strategy
|
||||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let githubAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const GitlabStrategy = require('passport-gitlab2').Strategy
|
||||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let gitlabAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
var GoogleStrategy = require('passport-google-oauth20').Strategy
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let googleAuth = module.exports = Router()
|
||||
|
||||
|
@ -12,14 +12,14 @@ passport.use(new GoogleStrategy({
|
|||
clientID: config.google.clientID,
|
||||
clientSecret: config.google.clientSecret,
|
||||
callbackURL: config.serverURL + '/auth/google/callback',
|
||||
userProfileURL: "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||
userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo'
|
||||
}, passportGeneralCallback))
|
||||
|
||||
googleAuth.get('/auth/google', function (req, res, next) {
|
||||
setReturnToFromReferer(req)
|
||||
passport.authenticate('google', { scope: ['profile'] })(req, res, next)
|
||||
})
|
||||
// google auth callback
|
||||
// google auth callback
|
||||
googleAuth.get('/auth/google/callback',
|
||||
passport.authenticate('google', {
|
||||
successReturnToOrRedirect: config.serverURL + '/',
|
||||
|
|
|
@ -6,8 +6,8 @@ const LDAPStrategy = require('passport-ldapauth')
|
|||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const {setReturnToFromReferer} = require('../utils')
|
||||
const {urlencodedParser} = require('../../utils')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const response = require('../../../response')
|
||||
|
||||
let ldapAuth = module.exports = Router()
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const Mattermost = require('mattermost')
|
||||
const OAuthStrategy = require('passport-oauth2').Strategy
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
const mattermost = new Mattermost.Client()
|
||||
|
||||
|
@ -24,12 +24,12 @@ mattermostStrategy.userProfile = (accessToken, done) => {
|
|||
mattermost.token = accessToken
|
||||
mattermost.useHeaderToken()
|
||||
mattermost.getMe(
|
||||
(data) => {
|
||||
done(null, data)
|
||||
},
|
||||
(err) => {
|
||||
done(err)
|
||||
}
|
||||
(data) => {
|
||||
done(null, data)
|
||||
},
|
||||
(err) => {
|
||||
done(err)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
const { Strategy, InternalOAuthError } = require('passport-oauth2')
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let oauth2Auth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
|
|||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const {urlencodedParser} = require('../../utils')
|
||||
const {setReturnToFromReferer} = require('../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
|
||||
let openIDAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ const SamlStrategy = require('passport-saml').Strategy
|
|||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const {urlencodedParser} = require('../../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const fs = require('fs')
|
||||
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const TwitterStrategy = require('passport-twitter').Strategy
|
||||
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let twitterAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const Router = require('express').Router
|
||||
|
||||
const {urlencodedParser} = require('./utils')
|
||||
const { urlencodedParser } = require('./utils')
|
||||
const history = require('../history')
|
||||
const historyRouter = module.exports = Router()
|
||||
|
||||
|
|
|
@ -17,12 +17,12 @@ exports.uploadImage = function (imagePath, callback) {
|
|||
|
||||
imgur.setClientId(config.imgur.clientID)
|
||||
imgur.uploadFile(imagePath)
|
||||
.then(function (json) {
|
||||
if (config.debug) {
|
||||
logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
|
||||
}
|
||||
callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
|
||||
}).catch(function (err) {
|
||||
callback(new Error(err), null)
|
||||
})
|
||||
.then(function (json) {
|
||||
if (config.debug) {
|
||||
logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
|
||||
}
|
||||
callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
|
||||
}).catch(function (err) {
|
||||
callback(new Error(err), null)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ const fs = require('fs')
|
|||
const path = require('path')
|
||||
|
||||
const config = require('../../config')
|
||||
const {getImageMimeType} = require('../../utils')
|
||||
const { getImageMimeType } = require('../../utils')
|
||||
const logger = require('../../logger')
|
||||
|
||||
const Minio = require('minio')
|
||||
|
|
|
@ -3,7 +3,7 @@ const fs = require('fs')
|
|||
const path = require('path')
|
||||
|
||||
const config = require('../../config')
|
||||
const {getImageMimeType} = require('../../utils')
|
||||
const { getImageMimeType } = require('../../utils')
|
||||
const logger = require('../../logger')
|
||||
|
||||
const AWS = require('aws-sdk')
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
const toobusy = require('toobusy-js')
|
||||
|
||||
const response = require('../../response')
|
||||
const config = require('../../config')
|
||||
|
||||
toobusy.maxLag(config.tooBusyLag)
|
||||
|
||||
module.exports = function (req, res, next) {
|
||||
if (toobusy()) {
|
||||
|
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
|
||||
const response = require('../response')
|
||||
|
||||
const {markdownParser} = require('./utils')
|
||||
const { markdownParser } = require('./utils')
|
||||
|
||||
const noteRouter = module.exports = Router()
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const config = require('../config')
|
|||
const models = require('../models')
|
||||
const logger = require('../logger')
|
||||
|
||||
const {urlencodedParser} = require('./utils')
|
||||
const { urlencodedParser } = require('./utils')
|
||||
|
||||
const statusRouter = module.exports = Router()
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const response = require('../response')
|
|||
const config = require('../config')
|
||||
const models = require('../models')
|
||||
const logger = require('../logger')
|
||||
const {generateAvatar} = require('../letter-avatars')
|
||||
const { generateAvatar } = require('../letter-avatars')
|
||||
|
||||
const UserRouter = module.exports = Router()
|
||||
|
||||
|
|
223
locales/es.json
223
locales/es.json
|
@ -1,104 +1,121 @@
|
|||
{
|
||||
"Collaborative markdown notes": "Notas colaborativas en Markdown",
|
||||
"Realtime collaborative markdown notes on all platforms.": "Notas colaborativas en Markdown para todas las plataformas.",
|
||||
"Best way to write and share your knowledge in markdown.": "La mejor forma de escribir y compartir tu conocimiento en Markdown.",
|
||||
"Intro": "Introducción",
|
||||
"History": "Historia",
|
||||
"New guest note": "Nueva nota como invitado",
|
||||
"Collaborate with URL": "Colaborar via URL",
|
||||
"Support charts and MathJax": "Soporte para gráficos y MathJax",
|
||||
"Support slide mode": "Soporte para diapositivas",
|
||||
"Sign In": "Ingresar",
|
||||
"Below is the history from browser": "A continuación se muestra el historial del navegador",
|
||||
"Welcome!": "¡Bienvenido!",
|
||||
"New note": "Nueva nota",
|
||||
"or": "o",
|
||||
"Sign Out": "Salir",
|
||||
"Explore all features": "Explorar todas las funciones",
|
||||
"Select tags...": "Seleccionar etiquetas...",
|
||||
"Search keyword...": "Buscar palabras clave...",
|
||||
"Sort by title": "Ordenar por título",
|
||||
"Title": "Título",
|
||||
"Sort by time": "Ordenar por fecha",
|
||||
"Time": "Tiempo",
|
||||
"Export history": "Exportar historial",
|
||||
"Import history": "Importar historial",
|
||||
"Clear history": "Borrar historial",
|
||||
"Refresh history": "Actualizar historial",
|
||||
"No history": "Ningún historial",
|
||||
"Import from browser": "Importar del navegador",
|
||||
"Releases": "Versiones",
|
||||
"Are you sure?": "¿Estás seguro?",
|
||||
"Cancel": "Cancelar",
|
||||
"Yes, do it!": "Si, ¡hazlo!",
|
||||
"Choose method": "Elegir método",
|
||||
"Sign in via %s": "Ingresar via %s",
|
||||
"New": "Nuevo",
|
||||
"Publish": "Publicar",
|
||||
"Extra": "Extra",
|
||||
"Revision": "Revision",
|
||||
"Slide Mode": "Modo presentación",
|
||||
"Export": "Exportar",
|
||||
"Import": "Importar",
|
||||
"Clipboard": "Portapapeles",
|
||||
"Download": "Descargar",
|
||||
"Raw HTML": "HTML puro",
|
||||
"Edit": "Editar",
|
||||
"View": "Ver",
|
||||
"Both": "Ambos",
|
||||
"Help": "Ayuda",
|
||||
"Upload Image": "Subir imagen",
|
||||
"Menu": "Menú",
|
||||
"This page need refresh": "Esta página necesita ser cargada de nuevo",
|
||||
"You have an incompatible client version.": "Tienes una version del cliente incompatible.",
|
||||
"Refresh to update.": "Cargar de nuevo para actualizar.",
|
||||
"New version available!": "¡Nueva versión disponible!",
|
||||
"See releases notes here": "Ver aquí las notas de publicación",
|
||||
"Refresh to enjoy new features.": "Actualizar para usar las nuevas funciones.",
|
||||
"Your user state has changed.": "El estado de tu usuario ha cambiado.",
|
||||
"Refresh to load new user state.": "Recargar para actualizar el estado de tu usuario.",
|
||||
"Refresh": "Recargar",
|
||||
"Contacts": "Contactos",
|
||||
"Report an issue": "Reportar un problema",
|
||||
"Send us email": "Enviarnos un email",
|
||||
"Documents": "Documentos",
|
||||
"Features": "Funciones",
|
||||
"YAML Metadata": "Metadatos en YAML",
|
||||
"Slide Example": "Ejemplo de diapositiva",
|
||||
"Cheatsheet": "Ayudamemorias",
|
||||
"Example": "Ejemplo",
|
||||
"Syntax": "Sintaxis",
|
||||
"Header": "Cabecera",
|
||||
"Unordered List": "Lista desordenada",
|
||||
"Ordered List": "Lista ordenada",
|
||||
"Todo List": "Lista de tareas",
|
||||
"Blockquote": "Bloque de cita",
|
||||
"Bold font": "Fuente negrita",
|
||||
"Italics font": "Fuente itálica",
|
||||
"Strikethrough": "Tachado",
|
||||
"Inserted text": "Texto subrayado",
|
||||
"Marked text": "Texto marcado",
|
||||
"Link": "Enlace",
|
||||
"Image": "Imagen",
|
||||
"Code": "Código",
|
||||
"Externals": "Externos",
|
||||
"This is a alert area.": "Esto es un área de alerta.",
|
||||
"Revert": "Revertir",
|
||||
"Import from clipboard": "Importar del portapapeles",
|
||||
"Paste your markdown or webpage here...": "Pega tu markdown o página web aquí...",
|
||||
"Clear": "Limpiar",
|
||||
"This note is locked": "Esta nota está bloqueada",
|
||||
"Sorry, only owner can edit this note.": "Disculpa, solo el dueño puede editar esta nota.",
|
||||
"OK": "OK",
|
||||
"Reach the limit": "Haz alcanzado el límite",
|
||||
"Sorry, you've reached the max length this note can be.": "Disculpa, haz alcanzado la longitud máxima que puede tener esta nota.",
|
||||
"Please reduce the content or divide it to more notes, thank you!": "Por favor, reduce el contenido o dividela en mas notas, ¡gracias!",
|
||||
"Import from Gist": "Importar de un Gist",
|
||||
"Paste your gist url here...": "Pega el URL de tu Gist aquí...",
|
||||
"Import from Snippet": "Importar de Snippet",
|
||||
"Select From Available Projects": "Elegir de un proyecto disponible",
|
||||
"Select From Available Snippets": "Elegir de un Snippet disponible",
|
||||
"OR": "O",
|
||||
"Export to Snippet": "Exportar a Snippet",
|
||||
"Select Visibility Level": "Elegir el nivel de visibilidad"
|
||||
}
|
||||
"Collaborative markdown notes": "Notas colaborativas en Markdown",
|
||||
"Realtime collaborative markdown notes on all platforms.": "Notas colaborativas en Markdown para todas las plataformas en tiempo real.",
|
||||
"Best way to write and share your knowledge in markdown.": "La mejor forma de escribir y compartir tu conocimiento en Markdown.",
|
||||
"Intro": "Introducción",
|
||||
"History": "Historia",
|
||||
"New guest note": "Nueva nota como invitado",
|
||||
"Collaborate with URL": "Colaborar via URL",
|
||||
"Support charts and MathJax": "Soporte para gráficos y MathJax",
|
||||
"Support slide mode": "Soporte para diapositivas",
|
||||
"Sign In": "Ingresar",
|
||||
"Below is the history from browser": "A continuación se muestra el historial del navegador",
|
||||
"Welcome!": "¡Bienvenido!",
|
||||
"New note": "Nueva nota",
|
||||
"or": "o",
|
||||
"Sign Out": "Salir",
|
||||
"Explore all features": "Explorar todas las funciones",
|
||||
"Select tags...": "Seleccionar etiquetas...",
|
||||
"Search keyword...": "Buscar palabras clave...",
|
||||
"Sort by title": "Ordenar por título",
|
||||
"Title": "Título",
|
||||
"Sort by time": "Ordenar por fecha",
|
||||
"Time": "Tiempo",
|
||||
"Export history": "Exportar historial",
|
||||
"Import history": "Importar historial",
|
||||
"Clear history": "Borrar historial",
|
||||
"Refresh history": "Actualizar historial",
|
||||
"No history": "Ningún historial",
|
||||
"Import from browser": "Importar del navegador",
|
||||
"Releases": "Versiones",
|
||||
"Are you sure?": "¿Estás seguro?",
|
||||
"Do you really want to delete this note?": "¿Realmente quieres eliminar esta nota?",
|
||||
"All users will lose their connection.": "Todos los usuarios perderán su conexión.",
|
||||
"Cancel": "Cancelar",
|
||||
"Yes, do it!": "Si, ¡hazlo!",
|
||||
"Choose method": "Elegir método",
|
||||
"Sign in via %s": "Ingresar via %s",
|
||||
"New": "Nuevo",
|
||||
"Publish": "Publicar",
|
||||
"Extra": "Extra",
|
||||
"Revision": "Revision",
|
||||
"Slide Mode": "Modo presentación",
|
||||
"Export": "Exportar",
|
||||
"Import": "Importar",
|
||||
"Clipboard": "Portapapeles",
|
||||
"Download": "Descargar",
|
||||
"Raw HTML": "HTML puro",
|
||||
"Edit": "Editar",
|
||||
"View": "Ver",
|
||||
"Both": "Ambos",
|
||||
"Help": "Ayuda",
|
||||
"Upload Image": "Subir imagen",
|
||||
"Menu": "Menú",
|
||||
"This page need refresh": "Esta página necesita ser cargada de nuevo",
|
||||
"You have an incompatible client version.": "Tienes una version del cliente incompatible.",
|
||||
"Refresh to update.": "Cargar de nuevo para actualizar.",
|
||||
"New version available!": "¡Nueva versión disponible!",
|
||||
"See releases notes here": "Ver aquí las notas de publicación",
|
||||
"Refresh to enjoy new features.": "Actualizar para usar las nuevas funciones.",
|
||||
"Your user state has changed.": "El estado de tu usuario ha cambiado.",
|
||||
"Refresh to load new user state.": "Recargar para actualizar el estado de tu usuario.",
|
||||
"Refresh": "Recargar",
|
||||
"Contacts": "Contactos",
|
||||
"Report an issue": "Reportar un problema",
|
||||
"Meet us on %s": "Encuéntranos en %s",
|
||||
"Send us email": "Enviarnos un email",
|
||||
"Documents": "Documentos",
|
||||
"Features": "Funciones",
|
||||
"YAML Metadata": "Metadatos en YAML",
|
||||
"Slide Example": "Ejemplo de diapositiva",
|
||||
"Cheatsheet": "Ayudamemorias",
|
||||
"Example": "Ejemplo",
|
||||
"Syntax": "Sintaxis",
|
||||
"Header": "Cabecera",
|
||||
"Unordered List": "Lista desordenada",
|
||||
"Ordered List": "Lista ordenada",
|
||||
"Todo List": "Lista de tareas"< |