add changes from v0.3

This commit is contained in:
JJ Kasper
2018-11-24 00:23:32 -06:00
parent 73f05ce4a3
commit 111cf2ed35
133 changed files with 10768 additions and 7443 deletions

View File

@@ -1,2 +1 @@
node_modules node_modules
db

View File

@@ -1,3 +1 @@
.next .next
styles
config

32
.eslintrc.js Normal file
View File

@@ -0,0 +1,32 @@
module.exports = {
globals: {
app: false,
},
parser: 'babel-eslint',
plugins: ['react'],
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
settings: {
react: {
version: '16.7',
},
},
env: {
es6: true,
browser: true,
node: true,
mocha: true,
},
extends: ['eslint:recommended', 'plugin:react/recommended'],
rules: {
'no-console': 0,
'react/prop-types': 0,
'react/react-in-jsx-scope': 0,
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
},
}

View File

@@ -1,30 +0,0 @@
{
"globals": {
"app": false
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"es6": true,
"browser": true,
"node": true,
"mocha": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"rules": {
"react/prop-types": 0,
"react/react-in-jsx-scope": 0
}
}

5
.gitignore vendored
View File

@@ -4,6 +4,7 @@ logs
# Next build data (only add manually) # Next build data (only add manually)
.next .next
public/sw.js
# cloudflare ips cache file # cloudflare ips cache file
config/cfIPs.json config/cfIPs.json
@@ -12,6 +13,8 @@ config/cfIPs.json
pids pids
*.pid *.pid
*.seed *.seed
.sessions
.cache
# Directory for instrumented libs generated by jscoverage/JSCover # Directory for instrumented libs generated by jscoverage/JSCover
lib-cov lib-cov
@@ -114,6 +117,4 @@ $RECYCLE.BIN/
*.lnk *.lnk
# Others # Others
lib/
db
kb kb

View File

@@ -1 +0,0 @@
MdYEkay1bViLxxtsssdFt

View File

@@ -1,45 +0,0 @@
{
"devFiles": [],
"pages": {
"/_app": [
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
"static/chunks/commons.633cb95994571bd38b02.js",
"static/runtime/main-a0940d6708920bf8234a.js"
],
"/edit": [
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
"static/chunks/commons.633cb95994571bd38b02.js",
"static/runtime/main-a0940d6708920bf8234a.js"
],
"/index": [
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
"static/chunks/commons.633cb95994571bd38b02.js",
"static/runtime/main-a0940d6708920bf8234a.js"
],
"/k": [
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
"static/chunks/commons.633cb95994571bd38b02.js",
"static/runtime/main-a0940d6708920bf8234a.js"
],
"/new": [
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
"static/chunks/commons.633cb95994571bd38b02.js",
"static/runtime/main-a0940d6708920bf8234a.js"
],
"/settings": [
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
"static/chunks/commons.633cb95994571bd38b02.js",
"static/runtime/main-a0940d6708920bf8234a.js"
],
"/_error": [
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
"static/chunks/commons.633cb95994571bd38b02.js",
"static/runtime/main-a0940d6708920bf8234a.js"
],
"/": [
"static/runtime/webpack-d2e3d1ffeaa85b5443c6.js",
"static/chunks/commons.633cb95994571bd38b02.js",
"static/runtime/main-a0940d6708920bf8234a.js"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,848 +0,0 @@
{
"modules": {
"byIdentifier": {
"node_modules/react/index.js": 0,
"node_modules/glamor/lib/index.js": 1,
"node_modules/@babel/runtime/regenerator/index.js": 2,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/getUrl.js": 3,
"node_modules/core-js/library/modules/_core.js": 4,
"node_modules/@babel/runtime-corejs2/helpers/interopRequireDefault.js": 5,
"node_modules/prop-types/index.js": 6,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/theme.js": 7,
"node_modules/core-js/library/modules/_export.js": 8,
"node_modules/core-js/library/modules/_global.js": 9,
"node_modules/core-js/library/modules/_wks.js": 10,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/getJwt.js": 11,
"node_modules/react-redux/es/index.js 67b03e0c128d2b1ee84267ae4ef3254d": 12,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/redux/actions/userAct.js": 13,
"node_modules/@babel/runtime-corejs2/helpers/classCallCheck.js": 14,
"node_modules/@babel/runtime-corejs2/helpers/createClass.js": 15,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/mapUser.js": 16,
"node_modules/core-js/library/modules/_is-object.js": 17,
"node_modules/isomorphic-unfetch/browser.js": 18,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Page.js 7cd3bec4a0252faf8f91892a2f798527": 19,
"node_modules/core-js/library/modules/_object-dp.js": 20,
"node_modules/next/dynamic.js": 21,
"node_modules/next/router.js": 22,
"node_modules/core-js/library/modules/_an-object.js": 23,
"node_modules/@babel/runtime-corejs2/helpers/defineProperty.js": 24,
"node_modules/core-js/library/modules/_descriptors.js": 25,
"node_modules/core-js/library/modules/_ctx.js": 26,
"node_modules/@babel/runtime-corejs2/helpers/possibleConstructorReturn.js": 27,
"node_modules/@babel/runtime-corejs2/helpers/getPrototypeOf.js": 28,
"node_modules/@babel/runtime-corejs2/helpers/inherits.js": 29,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/PaddedRow.js": 30,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/redux/store.js 4e1280ed09dd33013b4dfa4dd70e85b4": 31,
"node_modules/@babel/runtime-corejs2/helpers/interopRequireWildcard.js": 32,
"node_modules/core-js/library/modules/_fails.js": 33,
"node_modules/core-js/library/modules/_hide.js": 34,
"node_modules/@babel/runtime-corejs2/helpers/objectSpread.js": 35,
"node_modules/core-js/library/modules/es6.string.iterator.js": 36,
"node_modules/next/dist/lib/utils.js": 37,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/updStateFromId.js": 38,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Markdown.js d5b5492a768bba52c33c5b22a90aa8a1": 39,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Spinner.js": 40,
"node_modules/core-js/library/modules/_to-iobject.js": 41,
"node_modules/core-js/library/modules/_has.js": 42,
"node_modules/core-js/library/modules/_a-function.js": 43,
"node_modules/core-js/library/modules/_to-object.js": 44,
"node_modules/core-js/library/modules/_iterators.js": 45,
"node_modules/@babel/runtime-corejs2/helpers/typeof.js": 46,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/checkDirParts.js": 47,
"node_modules/core-js/library/modules/_property-desc.js": 48,
"node_modules/@babel/runtime-corejs2/core-js/promise.js": 49,
"node_modules/core-js/library/modules/web.dom.iterable.js": 50,
"node_modules/@babel/runtime-corejs2/core-js/set.js": 51,
"node_modules/next/link.js": 52,
"node_modules/redux/es/redux.js": 53,
"node_modules/core-js/library/modules/_cof.js": 54,
"node_modules/core-js/library/modules/_library.js": 55,
"node_modules/core-js/library/modules/_set-to-string-tag.js": 56,
"node_modules/core-js/library/modules/_object-keys.js": 57,
"node_modules/core-js/library/modules/_to-length.js": 58,
"node_modules/core-js/library/modules/_object-create.js": 59,
"node_modules/@babel/runtime-corejs2/core-js/object/keys.js": 60,
"node_modules/core-js/library/modules/_classof.js": 61,
"node_modules/core-js/library/modules/_for-of.js": 62,
"node_modules/object-assign/index.js": 63,
"node_modules/next/dist/lib/router/index.js": 64,
"node_modules/@babel/runtime-corejs2/helpers/assertThisInitialized.js": 65,
"node_modules/css-in-js-utils/lib/isPrefixedValue.js": 66,
"node_modules/next/dist/lib/dynamic.js": 67,
"node_modules/url/url.js": 68,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/AddDoc.js": 69,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/MngDoc.js cfa406d5564443825298c8c4c761c7c2": 70,
"node_modules/core-js/library/modules/_object-pie.js": 71,
"node_modules/@babel/runtime-corejs2/core-js/object/define-property.js": 72,
"node_modules/core-js/library/modules/_uid.js": 73,
"node_modules/@babel/runtime-corejs2/regenerator/index.js": 74,
"node_modules/@babel/runtime-corejs2/helpers/asyncToGenerator.js": 75,
"node_modules/core-js/library/modules/core.get-iterator-method.js": 76,
"node_modules/@babel/runtime-corejs2/core-js/array/is-array.js": 77,
"node_modules/@babel/runtime-corejs2/core-js/object/assign.js": 78,
"node_modules/next/dist/lib/loadable.js": 79,
"node_modules/invariant/browser.js": 80,
"node_modules/core-js/library/modules/_iobject.js": 81,
"node_modules/core-js/library/modules/_defined.js": 82,
"node_modules/core-js/library/modules/_object-gopd.js": 83,
"node_modules/core-js/library/modules/_to-primitive.js": 84,
"node_modules/core-js/library/modules/_dom-create.js": 85,
"node_modules/core-js/library/modules/_object-sap.js": 86,
"node_modules/core-js/library/modules/_meta.js": 87,
"node_modules/core-js/library/modules/_shared.js": 88,
"node_modules/core-js/library/modules/_wks-ext.js": 89,
"node_modules/core-js/library/modules/_wks-define.js": 90,
"node_modules/core-js/library/modules/_to-integer.js": 91,
"node_modules/core-js/library/modules/_shared-key.js": 92,
"node_modules/core-js/library/modules/_enum-bug-keys.js": 93,
"node_modules/core-js/library/modules/_object-gops.js": 94,
"node_modules/core-js/library/modules/_is-array.js": 95,
"node_modules/core-js/library/modules/es6.object.to-string.js": 96,
"node_modules/core-js/library/modules/_iter-define.js": 97,
"node_modules/core-js/library/modules/_an-instance.js": 98,
"node_modules/core-js/library/modules/_iter-call.js": 99,
"node_modules/core-js/library/modules/_is-array-iter.js": 100,
"node_modules/core-js/library/modules/_new-promise-capability.js": 101,
"node_modules/core-js/library/modules/_redefine-all.js": 102,
"node_modules/core-js/library/modules/_iter-detect.js": 103,
"node_modules/@babel/runtime-corejs2/helpers/slicedToArray.js": 104,
"node_modules/webpack/buildin/global.js": 105,
"node_modules/next/dist/lib/EventEmitter.js": 106,
"node_modules/next/dist/lib/head.js": 107,
"node_modules/next/dist/lib/side-effect.js": 108,
"node_modules/@babel/runtime-corejs2/helpers/toConsumableArray.js": 109,
"node_modules/@babel/runtime-corejs2/helpers/arrayWithoutHoles.js": 110,
"node_modules/@babel/runtime-corejs2/helpers/iterableToArray.js": 111,
"node_modules/@babel/runtime-corejs2/core-js/array/from.js": 112,
"node_modules/core-js/library/fn/array/from.js": 113,
"node_modules/core-js/library/modules/es6.array.from.js": 114,
"node_modules/core-js/library/modules/_create-property.js": 115,
"node_modules/@babel/runtime-corejs2/core-js/is-iterable.js": 116,
"node_modules/core-js/library/fn/is-iterable.js": 117,
"node_modules/core-js/library/modules/core.is-iterable.js": 118,
"node_modules/@babel/runtime-corejs2/helpers/nonIterableSpread.js": 119,
"node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js": 120,
"node_modules/symbol-observable/es/index.js": 121,
"node_modules/next/app.js": 122,
"node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-descriptor.js": 123,
"node_modules/core-js/library/modules/_ie8-dom-define.js": 124,
"node_modules/core-js/library/modules/es6.symbol.js": 125,
"node_modules/core-js/library/modules/_redefine.js": 126,
"node_modules/core-js/library/modules/_object-keys-internal.js": 127,
"node_modules/core-js/library/modules/_html.js": 128,
"node_modules/core-js/library/modules/_object-gopn.js": 129,
"node_modules/regenerator-runtime/runtime-module.js": 130,
"node_modules/core-js/library/modules/_object-gpo.js": 131,
"node_modules/core-js/library/modules/_iter-step.js": 132,
"node_modules/core-js/library/modules/_species-constructor.js": 133,
"node_modules/core-js/library/modules/_task.js": 134,
"node_modules/core-js/library/modules/_invoke.js": 135,
"node_modules/core-js/library/modules/_perform.js": 136,
"node_modules/core-js/library/modules/_promise-resolve.js": 137,
"node_modules/core-js/library/modules/_set-species.js": 138,
"node_modules/@babel/runtime-corejs2/helpers/setPrototypeOf.js": 139,
"node_modules/@babel/runtime-corejs2/core-js/object/set-prototype-of.js": 140,
"node_modules/core-js/library/modules/_validate-collection.js": 141,
"node_modules/webpack/buildin/module.js": 142,
"node_modules/next/dist/lib/shallow-equals.js": 143,
"node_modules/glamor/lib/CSSPropertyOperations/index.js": 144,
"node_modules/fbjs/lib/warning.js": 145,
"node_modules/inline-style-prefixer/utils/capitalizeString.js": 146,
"node_modules/isomorphic-unfetch/node_modules/unfetch/dist/unfetch.es.js": 147,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/keys.js": 148,
"node_modules/next/head.js": 149,
"node_modules/symbol-observable/es/ponyfill.js": 150,
"node_modules/lodash-es/_freeGlobal.js": 151,
"node_modules/react-paginate/dist/index.js": 152,
"node_modules/process/browser.js": 153,
"multi node_modules/next/dist/client/next": 154,
"node_modules/next/dist/client/next.js": 155,
"node_modules/core-js/library/fn/object/get-own-property-descriptor.js": 156,
"node_modules/core-js/library/modules/es6.object.get-own-property-descriptor.js": 157,
"node_modules/core-js/library/fn/object/define-property.js": 158,
"node_modules/core-js/library/modules/es6.object.define-property.js": 159,
"node_modules/next/dist/client/index.js": 160,
"node_modules/@babel/runtime-corejs2/core-js/object/get-own-property-symbols.js": 161,
"node_modules/core-js/library/fn/object/get-own-property-symbols.js": 162,
"node_modules/core-js/library/modules/_enum-keys.js": 163,
"node_modules/core-js/library/modules/_array-includes.js": 164,
"node_modules/core-js/library/modules/_to-absolute-index.js": 165,
"node_modules/core-js/library/modules/_object-dps.js": 166,
"node_modules/core-js/library/modules/_object-gopn-ext.js": 167,
"node_modules/core-js/library/fn/object/keys.js": 168,
"node_modules/core-js/library/modules/es6.object.keys.js": 169,
"node_modules/regenerator-runtime/runtime.js": 170,
"node_modules/core-js/library/fn/promise.js": 171,
"node_modules/core-js/library/modules/_string-at.js": 172,
"node_modules/core-js/library/modules/_iter-create.js": 173,
"node_modules/core-js/library/modules/es6.array.iterator.js": 174,
"node_modules/core-js/library/modules/_add-to-unscopables.js": 175,
"node_modules/core-js/library/modules/es6.promise.js": 176,
"node_modules/core-js/library/modules/_microtask.js": 177,
"node_modules/core-js/library/modules/_user-agent.js": 178,
"node_modules/core-js/library/modules/es7.promise.finally.js": 179,
"node_modules/core-js/library/modules/es7.promise.try.js": 180,
"node_modules/@babel/runtime-corejs2/helpers/arrayWithHoles.js": 181,
"node_modules/core-js/library/fn/array/is-array.js": 182,
"node_modules/core-js/library/modules/es6.array.is-array.js": 183,
"node_modules/@babel/runtime-corejs2/helpers/iterableToArrayLimit.js": 184,
"node_modules/@babel/runtime-corejs2/core-js/get-iterator.js": 185,
"node_modules/core-js/library/fn/get-iterator.js": 186,
"node_modules/core-js/library/modules/core.get-iterator.js": 187,
"node_modules/@babel/runtime-corejs2/helpers/nonIterableRest.js": 188,
"node_modules/react/cjs/react.production.min.js": 189,
"node_modules/react-dom/index.js": 190,
"node_modules/react-dom/cjs/react-dom.production.min.js": 191,
"node_modules/schedule/index.js": 192,
"node_modules/schedule/cjs/schedule.production.min.js": 193,
"node_modules/next/dist/client/head-manager.js": 194,
"node_modules/@babel/runtime-corejs2/core-js/symbol/iterator.js": 195,
"node_modules/core-js/library/fn/symbol/iterator.js": 196,
"node_modules/@babel/runtime-corejs2/core-js/symbol.js": 197,
"node_modules/core-js/library/fn/symbol/index.js": 198,
"node_modules/core-js/library/modules/es7.symbol.async-iterator.js": 199,
"node_modules/core-js/library/modules/es7.symbol.observable.js": 200,
"node_modules/@babel/runtime-corejs2/helpers/construct.js": 201,
"node_modules/@babel/runtime-corejs2/core-js/reflect/construct.js": 202,
"node_modules/core-js/library/fn/reflect/construct.js": 203,
"node_modules/core-js/library/modules/es6.reflect.construct.js": 204,
"node_modules/core-js/library/modules/_bind.js": 205,
"node_modules/core-js/library/fn/object/set-prototype-of.js": 206,
"node_modules/core-js/library/modules/es6.object.set-prototype-of.js": 207,
"node_modules/core-js/library/modules/_set-proto.js": 208,
"node_modules/next/dist/lib/router/router.js": 209,
"node_modules/core-js/library/fn/set.js": 210,
"node_modules/core-js/library/modules/es6.set.js": 211,
"node_modules/core-js/library/modules/_collection-strong.js": 212,
"node_modules/core-js/library/modules/_collection.js": 213,
"node_modules/core-js/library/modules/_array-methods.js": 214,
"node_modules/core-js/library/modules/_array-species-create.js": 215,
"node_modules/core-js/library/modules/_array-species-constructor.js": 216,
"node_modules/core-js/library/modules/es7.set.to-json.js": 217,
"node_modules/core-js/library/modules/_collection-to-json.js": 218,
"node_modules/core-js/library/modules/_array-from-iterable.js": 219,
"node_modules/core-js/library/modules/es7.set.of.js": 220,
"node_modules/core-js/library/modules/_set-collection-of.js": 221,
"node_modules/core-js/library/modules/es7.set.from.js": 222,
"node_modules/core-js/library/modules/_set-collection-from.js": 223,
"node_modules/punycode/punycode.js": 224,
"node_modules/url/util.js": 225,
"node_modules/querystring-es3/index.js": 226,
"node_modules/querystring-es3/decode.js": 227,
"node_modules/querystring-es3/encode.js": 228,
"node_modules/next/dist/lib/p-queue.js": 229,
"node_modules/core-js/library/fn/object/assign.js": 230,
"node_modules/core-js/library/modules/es6.object.assign.js": 231,
"node_modules/core-js/library/modules/_object-assign.js": 232,
"node_modules/next/dist/lib/router/with-router.js": 233,
"node_modules/@babel/runtime-corejs2/core-js/object/get-prototype-of.js": 234,
"node_modules/core-js/library/fn/object/get-prototype-of.js": 235,
"node_modules/core-js/library/modules/es6.object.get-prototype-of.js": 236,
"node_modules/@babel/runtime-corejs2/core-js/object/create.js": 237,
"node_modules/core-js/library/fn/object/create.js": 238,
"node_modules/core-js/library/modules/es6.object.create.js": 239,
"node_modules/prop-types/factoryWithThrowingShims.js": 240,
"node_modules/prop-types/lib/ReactPropTypesSecret.js": 241,
"node_modules/next/dist/lib/page-loader.js": 242,
"node_modules/next/dist/lib/asset.js": 243,
"node_modules/next/dist/lib/runtime-config.js": 244,
"node_modules/next/dist/client/error-boundary.js": 245,
"multi ./pages/_app.js": 246,
"node_modules/glamor/lib/sheet.js": 247,
"node_modules/fbjs/lib/camelizeStyleName.js": 248,
"node_modules/fbjs/lib/camelize.js": 249,
"node_modules/glamor/lib/CSSPropertyOperations/dangerousStyleValue.js": 250,
"node_modules/glamor/lib/CSSPropertyOperations/CSSProperty.js": 251,
"node_modules/fbjs/lib/emptyFunction.js": 252,
"node_modules/fbjs/lib/hyphenateStyleName.js": 253,
"node_modules/fbjs/lib/hyphenate.js": 254,
"node_modules/fbjs/lib/memoizeStringOnly.js": 255,
"node_modules/glamor/lib/clean.js": 256,
"node_modules/glamor/lib/plugins.js": 257,
"node_modules/glamor/lib/prefixer.js": 258,
"node_modules/inline-style-prefixer/static/staticData.js": 259,
"node_modules/inline-style-prefixer/utils/prefixProperty.js": 260,
"node_modules/inline-style-prefixer/utils/prefixValue.js": 261,
"node_modules/inline-style-prefixer/static/plugins/cursor.js": 262,
"node_modules/inline-style-prefixer/static/plugins/crossFade.js": 263,
"node_modules/inline-style-prefixer/static/plugins/filter.js": 264,
"node_modules/inline-style-prefixer/static/plugins/flex.js": 265,
"node_modules/inline-style-prefixer/static/plugins/flexboxOld.js": 266,
"node_modules/inline-style-prefixer/static/plugins/gradient.js": 267,
"node_modules/inline-style-prefixer/static/plugins/imageSet.js": 268,
"node_modules/inline-style-prefixer/static/plugins/position.js": 269,
"node_modules/inline-style-prefixer/static/plugins/sizing.js": 270,
"node_modules/inline-style-prefixer/static/plugins/transition.js": 271,
"node_modules/css-in-js-utils/lib/hyphenateProperty.js": 272,
"node_modules/hyphenate-style-name/index.js": 273,
"node_modules/glamor/lib/hash.js": 274,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/milligram.js": 275,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/Roboto.js": 276,
"node_modules/url-join/lib/url-join.js": 277,
"node_modules/webpack/buildin/harmony-module.js": 278,
"node_modules/next/dist/lib/app.js": 279,
"node_modules/@babel/runtime-corejs2/helpers/extends.js": 280,
"multi ./pages/edit.js": 281,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/edit.js": 282,
"node_modules/next/dist/lib/link.js": 283,
"node_modules/@babel/runtime-corejs2/core-js/json/stringify.js": 284,
"node_modules/core-js/library/fn/json/stringify.js": 285,
"multi ./pages/index.js": 286,
"node_modules/react-paginate/dist/PaginationBoxView.js": 287,
"node_modules/react-paginate/dist/PageView.js": 288,
"node_modules/react-paginate/dist/BreakView.js": 289,
"multi ./pages/k.js": 290,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/k.js": 291,
"multi ./pages/new.js": 292,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/new.js": 293,
"multi ./pages/settings.js": 294,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/settings.js": 295,
"multi node_modules/next/dist/pages/_error.js": 296,
"node_modules/next/dist/pages/_error.js": 297,
"node_modules/next/error.js": 298,
"node_modules/next/dist/lib/error.js": 299,
"node_modules/http-status/lib/index.js": 300,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/index.js 63f90c0b7addefbc51a5877f453fdcbd": 301,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/_app.js 1362119e6d3d895366421ac2b14cb639": 302,
"node_modules/is-whitespace-character/index.js": 303,
"node_modules/xtend/immutable.js": 304,
"node_modules/trim/index.js": 305,
"node_modules/codemirror/lib/codemirror.js": 306,
"node_modules/is-decimal/index.js": 307,
"node_modules/unist-util-visit/index.js": 308,
"node_modules/parse-entities/index.js": 309,
"node_modules/repeat-string/index.js": 310,
"node_modules/trim-trailing-lines/index.js": 311,
"node_modules/remark-parse/lib/util/interrupt.js": 312,
"node_modules/remark-parse/lib/util/normalize.js": 313,
"node_modules/path-browserify/index.js": 314,
"node_modules/is-alphabetical/index.js": 315,
"node_modules/remark-parse/lib/defaults.js": 316,
"node_modules/remark-parse/lib/util/get-indentation.js": 317,
"node_modules/remark-parse/lib/util/html.js": 318,
"node_modules/remark-parse/lib/locate/tag.js": 319,
"node_modules/remark-parse/lib/locate/link.js": 320,
"node_modules/codemirror/mode/markdown/markdown.js": 321,
"node_modules/codemirror/mode/xml/xml.js": 322,
"node_modules/codemirror/mode/meta.js": 323,
"node_modules/unified/index.js": 324,
"node_modules/extend/index.js": 325,
"node_modules/bail/index.js": 326,
"node_modules/vfile/index.js": 327,
"node_modules/vfile-message/index.js": 328,
"node_modules/unist-util-stringify-position/index.js": 329,
"node_modules/vfile/core.js": 330,
"node_modules/replace-ext/index.js": 331,
"node_modules/is-buffer/index.js": 332,
"node_modules/trough/index.js": 333,
"node_modules/trough/wrap.js": 334,
"node_modules/x-is-string/index.js": 335,
"node_modules/is-plain-obj/index.js": 336,
"node_modules/remark-parse/index.js": 337,
"node_modules/unherit/index.js": 338,
"node_modules/inherits/inherits_browser.js": 339,
"node_modules/remark-parse/lib/parser.js": 340,
"node_modules/state-toggle/index.js": 341,
"node_modules/vfile-location/index.js": 342,
"node_modules/remark-parse/lib/unescape.js": 343,
"node_modules/remark-parse/lib/decode.js": 344,
"node_modules/character-entities/index.json": 345,
"node_modules/character-entities-legacy/index.json": 346,
"node_modules/character-reference-invalid/index.json": 347,
"node_modules/is-hexadecimal/index.js": 348,
"node_modules/is-alphanumerical/index.js": 349,
"node_modules/remark-parse/lib/tokenizer.js": 350,
"node_modules/remark-parse/lib/set-options.js": 351,
"node_modules/markdown-escapes/index.js": 352,
"node_modules/remark-parse/lib/block-elements.json": 353,
"node_modules/remark-parse/lib/parse.js": 354,
"node_modules/unist-util-remove-position/index.js": 355,
"node_modules/unist-util-visit/node_modules/unist-util-visit-parents/index.js": 356,
"node_modules/unist-util-is/index.js": 357,
"node_modules/remark-parse/lib/tokenize/newline.js": 358,
"node_modules/remark-parse/lib/tokenize/code-indented.js": 359,
"node_modules/remark-parse/lib/tokenize/code-fenced.js": 360,
"node_modules/remark-parse/lib/tokenize/blockquote.js": 361,
"node_modules/remark-parse/lib/tokenize/heading-atx.js": 362,
"node_modules/remark-parse/lib/tokenize/thematic-break.js": 363,
"node_modules/remark-parse/lib/tokenize/list.js": 364,
"node_modules/remark-parse/lib/util/remove-indentation.js": 365,
"node_modules/remark-parse/lib/tokenize/heading-setext.js": 366,
"node_modules/remark-parse/lib/tokenize/html-block.js": 367,
"node_modules/remark-parse/lib/tokenize/footnote-definition.js": 368,
"node_modules/collapse-white-space/index.js": 369,
"node_modules/remark-parse/lib/tokenize/definition.js": 370,
"node_modules/remark-parse/lib/tokenize/table.js": 371,
"node_modules/remark-parse/lib/tokenize/paragraph.js": 372,
"node_modules/remark-parse/lib/tokenize/escape.js": 373,
"node_modules/remark-parse/lib/locate/escape.js": 374,
"node_modules/remark-parse/lib/tokenize/auto-link.js": 375,
"node_modules/remark-parse/lib/tokenize/url.js": 376,
"node_modules/remark-parse/lib/locate/url.js": 377,
"node_modules/remark-parse/lib/tokenize/html-inline.js": 378,
"node_modules/remark-parse/lib/tokenize/link.js": 379,
"node_modules/remark-parse/lib/tokenize/reference.js": 380,
"node_modules/remark-parse/lib/tokenize/strong.js": 381,
"node_modules/remark-parse/lib/locate/strong.js": 382,
"node_modules/remark-parse/lib/tokenize/emphasis.js": 383,
"node_modules/is-word-character/index.js": 384,
"node_modules/remark-parse/lib/locate/emphasis.js": 385,
"node_modules/remark-parse/lib/tokenize/delete.js": 386,
"node_modules/remark-parse/lib/locate/delete.js": 387,
"node_modules/remark-parse/lib/tokenize/code-inline.js": 388,
"node_modules/remark-parse/lib/locate/code-inline.js": 389,
"node_modules/remark-parse/lib/tokenize/break.js": 390,
"node_modules/remark-parse/lib/locate/break.js": 391,
"node_modules/remark-parse/lib/tokenize/text.js": 392,
"node_modules/mdast-add-list-metadata/index.js": 393,
"node_modules/unist-util-visit-parents/index.js": 394,
"node_modules/react-markdown/lib/plugins/naive-html.js": 395,
"node_modules/react-markdown/lib/plugins/disallow-node.js": 396,
"node_modules/react-markdown/lib/ast-to-react.js": 397,
"node_modules/react-markdown/lib/wrap-table-rows.js": 398,
"node_modules/react-markdown/lib/get-definitions.js": 399,
"node_modules/react-markdown/lib/uriTransformer.js": 400,
"node_modules/react-markdown/lib/renderers.js": 401,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/CodeMirror.js": 402,
"node_modules/react-markdown/lib/react-markdown.js": 403
},
"usedIds": {
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
"11": 11,
"12": 12,
"13": 13,
"14": 14,
"15": 15,
"16": 16,
"17": 17,
"18": 18,
"19": 19,
"20": 20,
"21": 21,
"22": 22,
"23": 23,
"24": 24,
"25": 25,
"26": 26,
"27": 27,
"28": 28,
"29": 29,
"30": 30,
"31": 31,
"32": 32,
"33": 33,
"34": 34,
"35": 35,
"36": 36,
"37": 37,
"38": 38,
"39": 39,
"40": 40,
"41": 41,
"42": 42,
"43": 43,
"44": 44,
"45": 45,
"46": 46,
"47": 47,
"48": 48,
"49": 49,
"50": 50,
"51": 51,
"52": 52,
"53": 53,
"54": 54,
"55": 55,
"56": 56,
"57": 57,
"58": 58,
"59": 59,
"60": 60,
"61": 61,
"62": 62,
"63": 63,
"64": 64,
"65": 65,
"66": 66,
"67": 67,
"68": 68,
"69": 69,
"70": 70,
"71": 71,
"72": 72,
"73": 73,
"74": 74,
"75": 75,
"76": 76,
"77": 77,
"78": 78,
"79": 79,
"80": 80,
"81": 81,
"82": 82,
"83": 83,
"84": 84,
"85": 85,
"86": 86,
"87": 87,
"88": 88,
"89": 89,
"90": 90,
"91": 91,
"92": 92,
"93": 93,
"94": 94,
"95": 95,
"96": 96,
"97": 97,
"98": 98,
"99": 99,
"100": 100,
"101": 101,
"102": 102,
"103": 103,
"104": 104,
"105": 105,
"106": 106,
"107": 107,
"108": 108,
"109": 109,
"110": 110,
"111": 111,
"112": 112,
"113": 113,
"114": 114,
"115": 115,
"116": 116,
"117": 117,
"118": 118,
"119": 119,
"120": 120,
"121": 121,
"122": 122,
"123": 123,
"124": 124,
"125": 125,
"126": 126,
"127": 127,
"128": 128,
"129": 129,
"130": 130,
"131": 131,
"132": 132,
"133": 133,
"134": 134,
"135": 135,
"136": 136,
"137": 137,
"138": 138,
"139": 139,
"140": 140,
"141": 141,
"142": 142,
"143": 143,
"144": 144,
"145": 145,
"146": 146,
"147": 147,
"148": 148,
"149": 149,
"150": 150,
"151": 151,
"152": 152,
"153": 153,
"154": 154,
"155": 155,
"156": 156,
"157": 157,
"158": 158,
"159": 159,
"160": 160,
"161": 161,
"162": 162,
"163": 163,
"164": 164,
"165": 165,
"166": 166,
"167": 167,
"168": 168,
"169": 169,
"170": 170,
"171": 171,
"172": 172,
"173": 173,
"174": 174,
"175": 175,
"176": 176,
"177": 177,
"178": 178,
"179": 179,
"180": 180,
"181": 181,
"182": 182,
"183": 183,
"184": 184,
"185": 185,
"186": 186,
"187": 187,
"188": 188,
"189": 189,
"190": 190,
"191": 191,
"192": 192,
"193": 193,
"194": 194,
"195": 195,
"196": 196,
"197": 197,
"198": 198,
"199": 199,
"200": 200,
"201": 201,
"202": 202,
"203": 203,
"204": 204,
"205": 205,
"206": 206,
"207": 207,
"208": 208,
"209": 209,
"210": 210,
"211": 211,
"212": 212,
"213": 213,
"214": 214,
"215": 215,
"216": 216,
"217": 217,
"218": 218,
"219": 219,
"220": 220,
"221": 221,
"222": 222,
"223": 223,
"224": 224,
"225": 225,
"226": 226,
"227": 227,
"228": 228,
"229": 229,
"230": 230,
"231": 231,
"232": 232,
"233": 233,
"234": 234,
"235": 235,
"236": 236,
"237": 237,
"238": 238,
"239": 239,
"240": 240,
"241": 241,
"242": 242,
"243": 243,
"244": 244,
"245": 245,
"246": 246,
"247": 247,
"248": 248,
"249": 249,
"250": 250,
"251": 251,
"252": 252,
"253": 253,
"254": 254,
"255": 255,
"256": 256,
"257": 257,
"258": 258,
"259": 259,
"260": 260,
"261": 261,
"262": 262,
"263": 263,
"264": 264,
"265": 265,
"266": 266,
"267": 267,
"268": 268,
"269": 269,
"270": 270,
"271": 271,
"272": 272,
"273": 273,
"274": 274,
"275": 275,
"276": 276,
"277": 277,
"278": 278,
"279": 279,
"280": 280,
"281": 281,
"282": 282,
"283": 283,
"284": 284,
"285": 285,
"286": 286,
"287": 287,
"288": 288,
"289": 289,
"290": 290,
"291": 291,
"292": 292,
"293": 293,
"294": 294,
"295": 295,
"296": 296,
"297": 297,
"298": 298,
"299": 299,
"300": 300,
"301": 301,
"302": 302,
"303": 303,
"304": 304,
"305": 305,
"306": 306,
"307": 307,
"308": 308,
"309": 309,
"310": 310,
"311": 311,
"312": 312,
"313": 313,
"314": 314,
"315": 315,
"316": 316,
"317": 317,
"318": 318,
"319": 319,
"320": 320,
"321": 321,
"322": 322,
"323": 323,
"324": 324,
"325": 325,
"326": 326,
"327": 327,
"328": 328,
"329": 329,
"330": 330,
"331": 331,
"332": 332,
"333": 333,
"334": 334,
"335": 335,
"336": 336,
"337": 337,
"338": 338,
"339": 339,
"340": 340,
"341": 341,
"342": 342,
"343": 343,
"344": 344,
"345": 345,
"346": 346,
"347": 347,
"348": 348,
"349": 349,
"350": 350,
"351": 351,
"352": 352,
"353": 353,
"354": 354,
"355": 355,
"356": 356,
"357": 357,
"358": 358,
"359": 359,
"360": 360,
"361": 361,
"362": 362,
"363": 363,
"364": 364,
"365": 365,
"366": 366,
"367": 367,
"368": 368,
"369": 369,
"370": 370,
"371": 371,
"372": 372,
"373": 373,
"374": 374,
"375": 375,
"376": 376,
"377": 377,
"378": 378,
"379": 379,
"380": 380,
"381": 381,
"382": 382,
"383": 383,
"384": 384,
"385": 385,
"386": 386,
"387": 387,
"388": 388,
"389": 389,
"390": 390,
"391": 391,
"392": 392,
"393": 393,
"394": 394,
"395": 395,
"396": 396,
"397": 397,
"398": 398,
"399": 399,
"400": 400,
"401": 401,
"402": 402,
"403": 403
}
},
"chunks": {
"byName": {
"commons": 0,
"static/runtime/webpack.js": 1,
"static/runtime/main.js": 2,
"static/MdYEkay1bViLxxtsssdFt/pages/_app.js": 3,
"static/MdYEkay1bViLxxtsssdFt/pages/edit.js": 4,
"static/MdYEkay1bViLxxtsssdFt/pages/index.js": 5,
"static/MdYEkay1bViLxxtsssdFt/pages/k.js": 6,
"static/MdYEkay1bViLxxtsssdFt/pages/new.js": 7,
"static/MdYEkay1bViLxxtsssdFt/pages/settings.js": 8,
"static/MdYEkay1bViLxxtsssdFt/pages/_error.js": 9
},
"bySource": {
"0 node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/MngDoc.js ../components/CodeMirror": 10,
"0 node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Markdown.js react-markdown": 11
},
"usedIds": [
0,
1,
10,
11,
2,
3,
4,
5,
6,
7,
8,
9
]
}
}

View File

@@ -1 +0,0 @@
{"/_app":"static/MdYEkay1bViLxxtsssdFt/pages/_app.js","/_document":"static/MdYEkay1bViLxxtsssdFt/pages/_document.js","/edit":"static/MdYEkay1bViLxxtsssdFt/pages/edit.js","/index":"static/MdYEkay1bViLxxtsssdFt/pages/index.js","/k":"static/MdYEkay1bViLxxtsssdFt/pages/k.js","/new":"static/MdYEkay1bViLxxtsssdFt/pages/new.js","/settings":"static/MdYEkay1bViLxxtsssdFt/pages/settings.js","/_error":"static/MdYEkay1bViLxxtsssdFt/pages/_error.js","/":"static/MdYEkay1bViLxxtsssdFt/pages/index.js"}

View File

@@ -1,142 +0,0 @@
{
"modules": {
"byIdentifier": {
"external \"react\"": 0,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/getUrl.js": 1,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/redux/actions/userAct.js": 2,
"external \"glamor\"": 3,
"external \"@babel/runtime/regenerator\"": 4,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/theme.js": 5,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/redux/store.js 4e1280ed09dd33013b4dfa4dd70e85b4": 6,
"external \"react-redux\"": 7,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/mapUser.js": 8,
"external \"isomorphic-unfetch\"": 9,
"external \"next/router\"": 10,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/PaddedRow.js": 11,
"external \"redux\"": 12,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Spinner.js": 13,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/getJwt.js": 14,
"external \"next/link\"": 15,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Page.js 7cd3bec4a0252faf8f91892a2f798527": 16,
"external \"url\"": 17,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/keys.js": 18,
"external \"url-join\"": 19,
"external \"next/dynamic\"": 20,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/updStateFromId.js": 21,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/Markdown.js d5b5492a768bba52c33c5b22a90aa8a1": 22,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/util/checkDirParts.js": 23,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/AddDoc.js": 24,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/MngDoc.js cfa406d5564443825298c8c4c761c7c2": 25,
"external \"next/document\"": 26,
"external \"next/app\"": 27,
"external \"next/head\"": 28,
"external \"glamor/server\"": 29,
"external \"react-paginate\"": 30,
"external \"codemirror\"": 31,
"external \"codemirror/mode/markdown/markdown\"": 32,
"external \"react-markdown\"": 33,
"multi ./pages/_app.js": 34,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/milligram.js": 35,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/styles/Roboto.js": 36,
"multi ./pages/_document.js": 37,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/_document.js": 38,
"multi ./pages/edit.js": 39,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/edit.js": 40,
"multi ./pages/index.js": 41,
"multi ./pages/k.js": 42,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/k.js": 43,
"multi ./pages/new.js": 44,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/new.js": 45,
"multi ./pages/settings.js": 46,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/settings.js": 47,
"multi node_modules/next/dist/pages/_error.js": 48,
"node_modules/next/dist/pages/_error.js": 49,
"external \"next/error\"": 50,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/index.js 63f90c0b7addefbc51a5877f453fdcbd": 51,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!pages/_app.js 1362119e6d3d895366421ac2b14cb639": 52,
"node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/CodeMirror.js": 53
},
"usedIds": {
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"10": 10,
"11": 11,
"12": 12,
"13": 13,
"14": 14,
"15": 15,
"16": 16,
"17": 17,
"18": 18,
"19": 19,
"20": 20,
"21": 21,
"22": 22,
"23": 23,
"24": 24,
"25": 25,
"26": 26,
"27": 27,
"28": 28,
"29": 29,
"30": 30,
"31": 31,
"32": 32,
"33": 33,
"34": 34,
"35": 35,
"36": 36,
"37": 37,
"38": 38,
"39": 39,
"40": 40,
"41": 41,
"42": 42,
"43": 43,
"44": 44,
"45": 45,
"46": 46,
"47": 47,
"48": 48,
"49": 49,
"50": 50,
"51": 51,
"52": 52,
"53": 53
}
},
"chunks": {
"byName": {
"static/MdYEkay1bViLxxtsssdFt/pages/_app.js": 0,
"static/MdYEkay1bViLxxtsssdFt/pages/_document.js": 1,
"static/MdYEkay1bViLxxtsssdFt/pages/edit.js": 2,
"static/MdYEkay1bViLxxtsssdFt/pages/index.js": 3,
"static/MdYEkay1bViLxxtsssdFt/pages/k.js": 4,
"static/MdYEkay1bViLxxtsssdFt/pages/new.js": 5,
"static/MdYEkay1bViLxxtsssdFt/pages/settings.js": 6,
"static/MdYEkay1bViLxxtsssdFt/pages/_error.js": 7
},
"bySource": {
"0 node_modules/next/dist/build/webpack/loaders/next-babel-loader.js??ref--4!src/components/MngDoc.js ../components/CodeMirror": 8
},
"usedIds": [
0,
1,
2,
3,
4,
5,
6,
7,
8
]
}
}

View File

@@ -1,4 +0,0 @@
/* This cache is used by webpack for instantiated modules */
module.exports = {}

View File

@@ -1,23 +1,30 @@
FROM node:8-alpine FROM node:8-alpine
RUN apk add yarn git bash s6 # install build stuff ( make python2 g++ ) for bcrypt since
# alpine linux doesn't have pre-built bcrypt
RUN apk add git bash s6 make python2 g++
RUN mkdir -p /opt/mykb RUN mkdir -p /opt/mykb
# install node_modules to tmp so it can be cached # install node_modules to tmp so it can be cached
RUN mkdir -p /tmp/mykb RUN mkdir -p /tmp/mykb
COPY package.json /tmp/mykb COPY package.json /tmp/mykb
RUN cd /tmp/mykb && yarn RUN cd /tmp/mykb && npm install
RUN mv /tmp/mykb/node_modules /opt/mykb/ RUN mv /tmp/mykb/node_modules /opt/mykb/
COPY . /opt/mykb COPY . /opt/mykb
RUN cd /opt/mykb && yarn build RUN cd /opt/mykb && npm run build
RUN cd /opt/mykb && npm prune --production
RUN npm cache clean --force
# remove packages from building bcrypt
RUN apk del make python2 g++
COPY docker_startup.sh /mykb COPY docker_startup.sh /mykb
RUN chmod +x /mykb RUN chmod +x /mykb
VOLUME /kb /db /config VOLUME /kb /config
EXPOSE 3030 EXPOSE 3000
ARG GIT_NAME=mykb ARG GIT_NAME=mykb
ARG GIT_EMAIL=mykb@localhost ARG GIT_EMAIL=mykb@localhost

View File

@@ -7,67 +7,63 @@
You can try the demo at: https://mykb.jjsweb.site You can try the demo at: https://mykb.jjsweb.site
\- Login \- Login
email: notadmin email: admin
password: secretpass password: secretpass
P.S. the demo is reset every 10 minutes P.S. the demo is reset every 10 minutes
## About ## About
MYKB is a simple file system/markdown based knowledge base editor/viewer built with [Feathers](http://feathersjs.com) and [Next.js](https://github.com/zeit/next.js) MYKB is a file system/markdown based knowledge base editor/viewer built with [Next.js](https://github.com/zeit/next.js)
Current features: Current features:
- live preview when editing a doc - live preview when editing a doc
- live file system watching - live file system watching
- caching of docs to speed up searching/viewing of docs - caching of docs to speed up searching/viewing of docs
- offline viewing of cached docs (requires browser that supports service workers)
- automatic git versioning - automatic git versioning
- automatic trusting of cloudflare reverse proxies - automatic trusting of Cloudflare reverse proxies
## Installing ## Installing
Getting up and running is as easy as 1, 2, 3 - With Docker
```
docker run --name mykb -v /path/to/docs:/kb -v /path/to/config:/config --env "PUID=USER_ID" --env "PGID=GROUP_ID" -p 3000:3000 ijjk/mykb:latest
```
1. Clone repo - With yarn (or npm)
```
git clone https://github.com/ijjk/mykb 1. Clone repo
``` ```
2. Install dependencies (omit `--prod` if developing) git clone https://github.com/ijjk/mykb
``` ```
cd path/to/mykb; npm i --prod 2. Install dependencies
``` ```
3. Start it cd path/to/mykb; yarn
``` ```
npm start 3. Build it
``` ```
yarn build && NODE_ENV=production node ./bin/genSecret.js
```
4. Start it
```
yarn start
```
## Options ## Options
host.json
| Name | Description |
| ---- | ----------- |
| host | The host to listen on |
| port | The port to listen on |
| pathPrefix | Used to prefix all urls for reverse proxies |
production.json (overrides default.json with production NODE_ENV var) production.json (overrides default.json with production NODE_ENV var)
| Name | Description | | Name | Description |
| ---- | ----------- | | ---- | ----------- |
| useGit | Whether or not to use a git repo to automatically version changes to docs (requires git to be installed) | | useGit | Whether or not to use a git repo to automatically version changes to docs (requires git to be installed) |
| docsDir | The directory where the markdown docs are located | | docsDir | The directory where the markdown docs are located |
| cacheSize | Max size of docs to store in memory for faster searching (default 7.5MB) | | cacheSize | Max size of docs to store in memory for faster searching (default 10MB) |
| trustCloudflare | Whether to trust X-Forwarded-For header from cloudflare IPs (used for rate limiting) | | trustCloudflare | Whether to trust X-Forwarded-For header from Cloudflare IPs (used for rate limiting) |
If using git the `user.email` and `user.name` configs need to be set either globally or on the docs repo
trustIPs.json - An array of [proxy-addr](https://www.npmjs.com/package/proxy-addr) compatible addresses to trust the X-Forwarded-For header from (Only needed if behind reverse proxy) trustIPs.json - An array of [proxy-addr](https://www.npmjs.com/package/proxy-addr) compatible addresses to trust the X-Forwarded-For header from (Only needed if behind reverse proxy)
## Testing
Simply run `npm test` and all your tests in the `test/` directory will be run
## License ## License
Copyright (c) 2017 Copyright (c) 2017

35
bin/genSecret.js Executable file
View File

@@ -0,0 +1,35 @@
#! /usr/bin/env node
const path = require('path')
const crypto = require('crypto')
const DB = require('../src/server/util/db')
const isDev = process.env.NODE_ENV !== 'production'
const configFile = (isDev ? 'default' : 'production') + '.json'
const configPath = path.join(__dirname, '../config', configFile)
const config = new DB(configPath)
config.loading
.then(() => {
const { secret } = config.data
if (!isDev && secret && !process.argv.some(arg => arg === '-f')) {
return console.log(
'Secret already exists, not updating. Use -f to force update'
)
}
config
.setData({
...config.data,
secret: crypto.randomBytes(256).toString('hex'),
})
.then(() => {
console.log(
`Authentication secret successfully updated in ${configPath}`
)
})
.catch(err => console.error(err))
})
.catch(err => {
console.error(err)
})

29
bin/genServiceWorker.js Normal file
View File

@@ -0,0 +1,29 @@
const path = require('path')
const fs = require('fs-extra')
const terser = require('terser')
const swPrecache = require('sw-precache')
const outputPath = path.resolve('public/sw.js')
async function main() {
let sw = await fs.readFile(path.resolve('src/client/util/offlineFallback.js'))
sw = sw.toString()
sw += await swPrecache.generate({
cacheId: 'mykb-cache',
stripPrefixMulti: {
[path.resolve('public/')]: '',
[path.resolve('.next/static/')]: '/_next/static',
},
staticFileGlobs: ['public/!(sw.js)', '.next/static/**/*'].map(p =>
path.resolve(p)
),
})
const precacheRegex = /var precacheConfig.*]];/
const precacheConfig = sw.match(precacheRegex)
sw = sw.replace(precacheRegex, '')
sw = precacheConfig[0] + sw
sw = terser.minify(sw).code
await fs.outputFile(outputPath, sw)
}
main()

View File

@@ -1,33 +1,25 @@
{ {
"docsDir": "../kb", "port": 3000,
"basePath": "/",
"docsDir": "./kb",
"cacheSize": 10000000,
"trustCloudflare": false, "trustCloudflare": false,
"cacheSize": 7500000, "maxDocsLimit": 50,
"paginate": { "defDocsLimit": 15,
"default": 10, "useGit": false,
"max": 50 "searchDelay": 100,
}, "date": {
"authentication": { "options": {
"secret": "030c8d13ff1eb57c5f7dfcd604005a034f2b1fcf89ce7898d30fab5908e33185d3eb94d9849a7d5aaac3c888e3a0d93031d32a04830609768abe6744513d2433f8dad73b8aab46f50d7d2cf4b58f20b2766815969db3dfa6653b110f305f17f757119004b862e6df3a3da6f0e1d9b24c98515fbf29729e42cc274ef28f87cdd84e48622f86102e7a046e1c83ed5e0c06921ad3093c1ffadfb8b2c1f9214c934dc3a118ed615d40f180d79c65f3a7ef2e30d10e3260e11be4535eeaabcd2ff16415760811a2a02817acad5701e735c40622f37c7b055d5cab3c7aa0539550c3e20479ceda988fdcd9c743b982f018a1800b8e4f4c08ccaae679f2fe1d798251cf", "month": "short",
"strategies": [ "day": "numeric",
"jwt", "year": "numeric"
"local"
],
"path": "/auth",
"service": "users",
"jwt": {
"header": {
"typ": "access"
},
"subject": "anonymous",
"issuer": "feathers",
"algorithm": "HS256",
"expiresIn": "1d"
}, },
"local": { "locale": "en-US"
"entity": "user",
"usernameField": "email",
"passwordField": "password"
}
}, },
"nedb": "../db" "jwt": {
"issuer": "mykb",
"expiresIn": "7d",
"audience": "mykb"
},
"secret": ""
} }

View File

@@ -1,5 +0,0 @@
{
"host": "localhost",
"port": 3030,
"pathPrefix": "/"
}

1
config/users.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,14 +1,12 @@
#!/bin/bash #!/bin/bash
PKGDIR="/opt/mykb" PKGDIR="/opt/mykb"
DBDIR="$PKGDIR/db"
KBDIR="$PKGDIR/kb" KBDIR="$PKGDIR/kb"
CONFDIR="$PKGDIR/config" CONFDIR="$PKGDIR/config"
if [ -d "/db" ];then # check if kb volume exists and is empty and copy starter doc if it is
rm -rf $DBDIR if [ -d "/kb" ] && [ -z "$(ls -A /kb)" ] && [ -f "/opt/mykb/kb/hello world.md" ];then
ln -s /db $DBDIR cp "/opt/mykb/kb/hello world.md" /kb/
DBDIR="/db"
fi fi
if [ -d "/kb" ];then if [ -d "/kb" ];then
@@ -29,12 +27,12 @@ export NODE_ENV=production
if [ -z "$PUID" ];then if [ -z "$PUID" ];then
echo 'no PUID set running as default user' echo 'no PUID set running as default user'
node ./genSecret.js && node ./src node ./bin/genSecret.js && node ./src
else else
echo 'chowning files' echo 'chowning files'
DIRS=($KBDIR $DBDIR $CONFDIR) DIRS=($KBDIR $CONFDIR)
for dir in ${DIRS[@]};do chown "$PUID:$PGID" $dir;done for dir in ${DIRS[@]};do chown "$PUID:$PGID" $dir;done
chown "$PUID:$PGID" -R $CONFDIR chown "$PUID:$PGID" -R $CONFDIR
s6-setuidgid "$PUID:$PGID" node ./genSecret.js s6-setuidgid "$PUID:$PGID" node ./bin/genSecret.js
s6-setuidgid "$PUID:$PGID" node ./src s6-setuidgid "$PUID:$PGID" node ./src
fi fi

View File

@@ -1,28 +0,0 @@
/* eslint-disable no-console */
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const secret = crypto.randomBytes(256).toString('hex')
const isProd = process.env.NODE_ENV === 'production'
const confFile = (isProd ? 'production' : 'default') + '.json'
const config = require('./config/' + confFile)
const configPath = path.join(__dirname, 'config', confFile)
// if in production check if secret exists and -f switch is set
if (isProd && config.authentication && config.authentication.secret) {
if (!process.argv.some(arg => arg === '-f')) {
return console.log(
'Secret already exists, not updating. Use -f to force update'
)
}
}
if (!config.authentication) {
config.authentication = { secret }
} else {
config.authentication.secret = secret
}
fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', err => {
if (err) return console.error(err)
console.log('\nAuthentication secret successfully updated in', confFile)
})

View File

@@ -1,84 +1,88 @@
{ {
"name": "mykb", "name": "mykb",
"description": "A file system/markdown based knowledge base editor/viewer",
"version": "0.3.0", "version": "0.3.0",
"description": "A file system/markdown based knowledge base editor/viewer",
"main": "src", "main": "src",
"keywords": [ "keywords": [
"feathers",
"react", "react",
"next.js", "react-hooks",
"markdown" "markdown",
"express",
"next"
], ],
"engines": {
"node": ">= 8.0.0"
},
"scripts": {
"dev": "node src",
"build:clean": "rimraf .next",
"build:next": "next build",
"build:sw": "node bin/genServiceWorker.js",
"build": "run-s build:clean build:next build:sw",
"format-e": "eslint --fix .",
"format-p": "prettier --ignore-path .eslintignore --write '**/*.js'",
"format": "run-s format-p format-e",
"start": "cross-env NODE_ENV=production node src"
},
"lint-staged": {
"*.js": [
"prettier --write",
"eslint --fix",
"git add"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"author": { "author": {
"name": "JJ Kasper", "name": "JJ Kasper",
"email": "jj@jjsweb.site" "email": "jj@jjsweb.site"
}, },
"contributors": [], "repository": "https://github.com/ijjk/mykb",
"bugs": {}, "license": "MIT",
"directories": { "private": false,
"lib": "src",
"test": "test/"
},
"engines": {
"node": ">= 8.0.0",
"yarn": ">= 0.18.0"
},
"scripts": {
"format": "prettier --ignore-path .eslintignore --write '**/*.js'",
"lint": "eslint . --config .eslintrc.json",
"mocha": "cross-env NODE_ENV=production mocha test/ --recursive --exit",
"build": "next build",
"analyze": "cross-env ANALYZE=true next build",
"dev": "nodemon --ignore src/components --ignore src/redux --ignore src/styles --exec 'npm run start:dev'",
"start:dev": "node src/",
"start": "cross-env NODE_ENV=production node src/",
"test": "npm run lint && npm run build && npm run mocha",
"postinstall": "node ./genSecret.js; exit 0"
},
"dependencies": { "dependencies": {
"@feathersjs/authentication": "^2.1.5", "bcrypt": "^3.0.2",
"@feathersjs/authentication-jwt": "^2.0.0", "body-parser": "^1.18.3",
"@feathersjs/authentication-local": "^1.1.2", "chokidar": "^2.0.4",
"@feathersjs/configuration": "^1.0.2", "codemirror": "^5.41.0",
"@feathersjs/errors": "^3.3.0", "compression": "^1.7.3",
"@feathersjs/express": "^1.2.2",
"@feathersjs/feathers": "^3.1.4",
"chokidar": "^2.0.3",
"codemirror": "^5.37.0",
"compression": "^1.7.2",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"cors": "^2.8.4", "cross-env": "^5.2.0",
"cross-env": "^5.1.4", "express": "^4.16.4",
"express-rate-limit": "^2.11.0", "express-rate-limit": "^3.3.2",
"feathers-nedb": "^3.0.0", "fs-extra": "^7.0.1",
"fs-extra": "^5.0.0", "helmet": "^3.15.0",
"glamor": "^2.20.40", "isomorphic-unfetch": "^3.0.0",
"glob": "^7.1.2", "jsonwebtoken": "^8.3.0",
"helmet": "^3.12.0", "keymirror": "^0.1.1",
"if-env": "^1.0.4", "next": "^7.0.2",
"isomorphic-unfetch": "^2.0.0", "passport": "^0.4.0",
"milligram": "^1.3.0", "passport-jwt": "^4.0.0",
"nedb": "^1.8.0", "passport-local": "^1.0.0",
"next": "^7.0.0", "react": "^16.7.0-alpha.0",
"react": "^16.3.2", "react-dom": "^16.7.0-alpha.0",
"react-dom": "^16.3.2", "react-markdown": "^4.0.3",
"react-markdown": "^3.3.0", "react-paginate": "^6.0.0",
"react-paginate": "^5.2.3", "react-redux": "^5.1.0",
"react-redux": "^5.0.7", "redux": "^4.0.1",
"redux": "^4.0.0", "redux-localstorage": "^0.4.1",
"simple-git": "^1.92.0", "simple-git": "^1.107.0",
"url-join": "^4.0.0", "url-join": "^4.0.0",
"winston": "^2.4.1" "uuid": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^8.2.3", "babel-eslint": "^10.0.1",
"eslint": "^4.19.1", "eslint": "^5.8.0",
"eslint-plugin-react": "^7.8.2", "eslint-plugin-react": "^7.11.1",
"mocha": "^5.1.1", "husky": "^1.1.3",
"nodemon": "^1.17.3", "lint-staged": "^8.0.4",
"prettier": "^1.13.4", "npm-run-all": "^4.1.3",
"redux-logger": "^3.0.6", "prettier": "^1.15.1",
"request": "^2.85.0", "rimraf": "^2.6.2",
"request-promise": "^4.2.2" "server-destroy": "^1.0.1",
"sw-precache": "^5.2.1"
} }
} }

View File

@@ -1,50 +1,52 @@
import Head from 'next/head'
import '../src/styles/global'
import store from '../src/redux/store'
import { Provider } from 'react-redux'
import App, { Container } from 'next/app' import App, { Container } from 'next/app'
import { setUser, doLogin } from '../src/redux/actions/userAct' import Head from 'next/head'
import { Provider } from 'react-redux'
import withRedux from '../src/client/hocs/withRedux'
import Header from '../src/client/layout/header'
import Footer from '../src/client/layout/footer'
import Shortcuts from '../src/client/comps/shortcuts'
import Milligram from '../src/client/styles/milligram'
import Roboto from '../src/client/styles/roboto'
import Global from '../src/client/styles/global'
import '../src/client/util/registerServiceWorker'
const ssr = typeof window === 'undefined' class MyApp extends App {
export default class MyApp extends App {
static async getInitialProps({ Component, ctx }) { static async getInitialProps({ Component, ctx }) {
let user = {}
let setup = false
if (ssr) {
user = ctx.req.user || {}
setup = ctx.req.doSetup || false
}
let pageProps = {} let pageProps = {}
if (Component.getInitialProps) { if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx) pageProps = await Component.getInitialProps(ctx)
} }
return { Component, pageProps, user, setup } return { Component, pageProps }
}
componentWillMount() {
const { user, setup } = this.props
setUser({ ...user, setup })
if (!ssr && !user.email) {
const { jwt } = window.localStorage
if (jwt) doLogin(null, jwt, true)
}
} }
render() { render() {
let { Component, pageProps } = this.props const { Component, pageProps, reduxStore } = this.props
return ( return (
<Provider store={store}> <Provider store={reduxStore}>
<> <>
<Head> <Head>
<title>My Knowledge Base</title> <title>My Knowledge Base</title>
</Head> </Head>
<Container> <Header />
<Component {...pageProps} />
</Container> <div className="fill main">
<Container>
<Component {...pageProps} />
</Container>
</div>
<Footer />
<Shortcuts />
{/* style components */}
<Roboto />
<Milligram />
<Global />
</> </>
</Provider> </Provider>
) )
} }
} }
export default withRedux(MyApp)

View File

@@ -1,27 +1,13 @@
import getUrl from '../src/util/getUrl'
import { renderStaticOptimized } from 'glamor/server'
import Document, { Head, Main, NextScript } from 'next/document' import Document, { Head, Main, NextScript } from 'next/document'
import addBase from '../src/util/addBase'
export default class MyDocument extends Document { export default class MyDocument extends Document {
static async getInitialProps({ renderPage }) {
const page = renderPage()
const styles = renderStaticOptimized(() => page.html || page.errorHtml)
return { ...page, ...styles }
}
constructor(props) {
super(props)
const { __NEXT_DATA__, ids } = props
if (ids) {
__NEXT_DATA__.ids = this.props.ids
}
}
render() { render() {
return ( return (
<html> <html>
<Head> <Head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta <meta
name="viewport" name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no" content="width=device-width, initial-scale=1, shrink-to-fit=no"
@@ -29,35 +15,36 @@ export default class MyDocument extends Document {
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"
sizes="180x180" sizes="180x180"
href={getUrl('/apple-touch-icon.png')} href={addBase('/apple-touch-icon.png')}
/> />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="32x32" sizes="32x32"
href={getUrl('/favicon-32x32.png')} href={addBase('/favicon-32x32.png')}
/> />
<link <link
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="16x16" sizes="16x16"
href={getUrl('/favicon-16x16.png')} href={addBase('/favicon-16x16.png')}
/> />
<link rel="manifest" href={getUrl('/site.webmanifest')} /> <link rel="manifest" href={addBase('/site.webmanifest')} />
<link <link
rel="mask-icon" rel="mask-icon"
href={getUrl('/safari-pinned-tab.svg')} href={addBase('/safari-pinned-tab.svg')}
color="#00d1b2" color="#00d1b2"
/> />
<meta name="msapplication-TileColor" content="#202225" /> <meta name="msapplication-TileColor" content="#202225" />
<meta name="theme-color" content="#202225" /> <meta name="theme-color" content="#202225" />
<style
dangerouslySetInnerHTML={{ __html: this.props.css }}
data-glamor
/>
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: 'window.kbConf=' + JSON.stringify(app.get('kbConf')), __html:
'window.publicConfig=' +
JSON.stringify({
...global.publicConfig,
ssr: false,
}),
}} }}
/> />
</Head> </Head>

39
pages/_error.js Normal file
View File

@@ -0,0 +1,39 @@
import React from 'react'
export default class Error extends React.Component {
static getInitialProps({ res, err }) {
let statusCode = null
if (res) statusCode = res.statusCode
else if (err) statusCode = err.statusCode
return { statusCode }
}
render() {
const { statusCode } = this.props
return (
<div className="fill">
<h4>
{(() => {
if (statusCode === 404) {
return (
<>
<span>404</span> | This page could not be found
</>
)
}
return statusCode ? `Error: ${statusCode}` : 'An error occurred...'
})()}
</h4>
<style jsx>{`
div {
display: flex;
align-items: center;
justify-content: center;
}
`}</style>
</div>
)
}
}

51
pages/doc.js Normal file
View File

@@ -0,0 +1,51 @@
import React from 'react'
import Link from 'next/link'
import dynamic from 'next/dynamic'
import { connect } from 'react-redux'
import addBase from '../src/util/addBase'
import loadDocs from '../src/client/util/loadDocs'
import { deleteDoc } from '../src/client/util/docHelpers'
import RequireUser from '../src/client/comps/requireUser'
const Markdown = dynamic(() => import('react-markdown'))
function Doc({ cache, query }) {
return (
<RequireUser>
<div className="container fill padded">
{(() => {
const { id, md } = cache[query.id] || {}
if (!id) return <p>Doc was not found...</p>
return (
<div>
<h5>
{`${id} - `}
<Link
href={{ pathname: '/edit', query }}
as={{ pathname: addBase('/edit'), query }}
>
<a id="edit">edit</a>
</Link>
<button
className="float-right"
onClick={() => deleteDoc(query.id)}
>
DELETE
</button>
</h5>
<Markdown source={md} className="Markdown" />
</div>
)
})()}
</div>
</RequireUser>
)
}
Doc.getInitialProps = async ({ query }) => {
await loadDocs(query, true)
return { query }
}
export default connect(({ cache }) => ({ cache }))(Doc)

View File

@@ -1,18 +1,19 @@
import React, { Component } from 'react' import React from 'react'
import Page from '../src/components/Page' import EditDoc from '../src/client/comps/editDoc'
import MngDoc from '../src/components/MngDoc' import loadDocs from '../src/client/util/loadDocs'
import AddDoc from '../src/components/AddDoc' import RequireUser from '../src/client/comps/requireUser'
class Edit extends Component { function Edit({ query }) {
render() { return (
const { found, doc } = this.props <RequireUser>
if (!found) <EditDoc {...{ query }} />
return ( </RequireUser>
<Page> )
<h3>Doc not found...</h3>
</Page>
)
return <MngDoc {...{ doc }} />
}
} }
export default AddDoc(Edit)
Edit.getInitialProps = async ({ query }) => {
await loadDocs(query, true)
return { query }
}
export default Edit

View File

@@ -1,179 +1,22 @@
import React, { Component } from 'react' import React from 'react'
import { connect } from 'react-redux' import config from '../src/util/pubConfig'
import Router from 'next/router' import loadDocs from '../src/client/util/loadDocs'
import Paginate from 'react-paginate' import ListDocs from '../src/client/comps/listDocs'
import { format } from 'url' import RequireUser from '../src/client/comps/requireUser'
import Page from '../src/components/Page'
import PaddedRow from '../src/components/PaddedRow'
import Spinner from '../src/components/Spinner'
import DocItem from '../src/components/DocItem'
import { $limit, getDocs, buildQ } from '../src/util/getDocs'
import getJwt from '../src/util/getJwt'
import getUrl from '../src/util/getUrl'
import mapUser from '../src/util/mapUser'
class Index extends Component { function Home(props) {
state = { return (
$sort: 'updated:-1', <RequireUser>
$search: '', <ListDocs {...props} />
page: 1, </RequireUser>
pending: false, )
error: null,
total: 0,
docs: [],
}
static async getInitialProps({ req, query }) {
let page = 1,
$search = ''
if (query) {
page = query.page || page
$search = query.search || $search
}
const jwt = getJwt(req)
if (!jwt) return { total: 0, docs: [] }
const q = buildQ({ $search, $skip: page })
const data = await getDocs(q, req ? jwt : false)
return { ...data, page, $search }
}
static getDerivedStateFromProps(nextProps, prevState) {
let { docs, total, page, $search } = nextProps
if (
!prevState.didInit &&
(page !== prevState.page || $search !== prevState.$search)
) {
return { total, docs, page, $search, pending: false, didInit: true }
}
return null
}
pushQuery = query =>
Router.push(
{ pathname: '/', query },
format({ pathname: getUrl('/'), query })
)
updDocs = (time, doSearch) => {
clearTimeout(this.docsTime)
this.docsTime = setTimeout(async () => {
let { $sort, $search, page } = this.state
if (doSearch) {
const query = { search: $search }
if (!$search) delete query.search
this.pushQuery(query)
}
this.setState({ error: null })
this.docsTime = setTimeout(() => {
this.setState({ pending: true })
}, 125)
const q = buildQ({ $search, $sort, $skip: page })
const data = await getDocs(q)
clearTimeout(this.docsTime)
this.setState({ ...data, pending: false })
}, time || 275)
}
updQuery = e => {
this.setState({ [e.target.id]: e.target.value })
this.updDocs(0, e.target.id === '$search')
}
handlePage = ({ selected }) => {
const { $search } = this.state
const page = selected + 1
const query = {}
this.setState({ page })
if (page > 1) query.page = page
if ($search) query.search = $search
this.pushQuery(query)
this.updDocs(1)
}
componentDidMount() {
this.updDocs(1)
}
componentDidUpdate(prevProps) {
const { user, docs } = this.props
if (prevProps.user.email === user.email) return
if (user.email && docs.length === 0) this.updDocs(1)
}
render() {
const { $sort, $search, pending, error, docs, total, page } = this.state
const pages = Math.ceil(total / $limit)
return (
<Page>
<PaddedRow>
<input
type="text"
placeholder="Search knowledge base..."
maxLength={128}
value={$search}
className="search"
id="$search"
onChange={this.updQuery}
/>
</PaddedRow>
<PaddedRow>
<div className="inline" style={{ width: '100%' }}>
<h4 className="noMargin">Docs</h4>
<div className="float-right inline">
<label htmlFor="sort">Sort: </label>
<select
id="$sort"
value={$sort}
onChange={this.updQuery}
style={{ width: 150 }}
>
<option value="updated:-1">{'Updated (new -> old)'}</option>
<option value="updated:1">{'Updated (old -> new)'}</option>
<option value="created:-1">{'Created (new -> old)'}</option>
<option value="created:1">{'Created (old -> new)'}</option>
<option value="dirName:1">{'Name (A -> Z)'}</option>
<option value="dirName:-1">{'Name (Z -> A)'}</option>
</select>
</div>
</div>
</PaddedRow>
<PaddedRow>
{docs.length > 0 || error || pending ? null : <p>No docs found...</p>}
{!error ? null : <p>{error}</p>}
{!pending || error ? null : (
<Spinner style={{ margin: '25px auto 0' }} />
)}
{docs.length < 1 || pending || error ? null : (
<div>
<table>
<thead>
<tr>
<th>
Doc <span className="float-right">Modified</span>
</th>
</tr>
</thead>
<tbody>
{docs.map(doc => (
<DocItem {...doc} key={doc.id} />
))}
</tbody>
</table>
{pages < 2 ? null : (
<Paginate
pageCount={pages}
containerClassName="paginate"
activeClassName="active"
onPageChange={this.handlePage}
forcePage={page - 1}
/>
)}
</div>
)}
</PaddedRow>
</Page>
)
}
} }
export default connect(mapUser)(Index)
Home.getInitialProps = async ctx => {
if (config.ssr) {
await loadDocs(ctx.query || {})
}
return { query: ctx.query }
}
export default Home

View File

@@ -1,65 +0,0 @@
import React, { Component } from 'react'
import Link from 'next/link'
import Router from 'next/router'
import fetch from 'isomorphic-unfetch'
import Page from '../src/components/Page'
import Markdown from '../src/components/Markdown'
import AddDoc from '../src/components/AddDoc'
import getUrl from '../src/util/getUrl'
import getJwt from '../src/util/getJwt'
class k extends Component {
delete = async () => {
const sure = window.confirm(
'Are you sure you want to delete this doc? This can not be undone.'
)
if (!sure) return
const del = await fetch(getUrl('docs/' + this.props.id), {
headers: { Authorization: getJwt() },
method: 'DELETE',
}).catch(({ message }) => ({ ok: false, message }))
if (del.ok) Router.push('/', getUrl('/'))
else {
if (!del.message) {
const data = await del.json()
del.message = data.message
}
window.alert(`Could not delete doc, ${del.message}`)
}
}
render() {
const { found, id, doc } = this.props
if (!found)
return (
<Page>
<h3>Doc not found...</h3>
</Page>
)
return (
<Page>
<h5 style={{ marginBottom: '1rem' }}>
{doc.dir}
{doc.dir.length > 0 ? '/' : ''}
{doc.name}
{' - '}
<Link
as={getUrl('edit/' + id)}
href={{ pathname: '/edit', query: { id } }}
>
<a id="edit">edit</a>
</Link>
<button
className="float-right"
onClick={this.delete}
style={{ margin: '5px 0 0' }}
>
Delete
</button>
</h5>
<Markdown source={doc.md} className="Markdown" />
</Page>
)
}
}
export default AddDoc(k)

View File

@@ -1,2 +1,15 @@
import MngDoc from '../src/components/MngDoc' import React from 'react'
export default MngDoc import EditDoc from '../src/client/comps/editDoc'
import RequireUser from '../src/client/comps/requireUser'
function New({ query }) {
return (
<RequireUser>
<EditDoc {...{ query }} />
</RequireUser>
)
}
New.getInitialProps = async ({ query }) => ({ query })
export default New

23
pages/offline.js Normal file
View File

@@ -0,0 +1,23 @@
import { useEffect } from 'react'
import Router from 'next/router'
import addBase from '../src/util/addBase'
export default function Offline() {
// force next to render correct route on mount
useEffect(() => {
const { pathname, search } = window.location
let origRoute = pathname + search
let curRoute = origRoute.split(addBase('/'))
curRoute.splice(0, 1)
curRoute = '/' + curRoute.join(addBase('/'))
if (curRoute === '/offline' && navigator.onLine) {
curRoute = '/'
origRoute = '/'
}
Router.push(curRoute, origRoute)
}, [])
return null
}

View File

@@ -1,114 +1,128 @@
import React, { Component } from 'react' import React, { useState } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import fetch from 'isomorphic-unfetch' import addBase from '../src/util/addBase'
import Page from '../src/components/Page' import getHeaders from '../src/client/util/getHeaders'
import PaddedRow from '../src/components/PaddedRow' import RequireUser from '../src/client/comps/requireUser'
import Spinner from '../src/components/Spinner'
import updStateFromId from '../src/util/updStateFromId'
import mapUser from '../src/util/mapUser'
import getUrl from '../src/util/getUrl'
import getJwt from '../src/util/getJwt'
class Settings extends Component { function Settings({ user }) {
state = { const [data, setData] = useState({
pending: false, current: '',
passErr: null, confirm: '',
curPass: '', new: '',
newPass: '', })
confPass: '', const [pending, setPending] = useState(false)
} const [error, setError] = useState(null)
updVal = updStateFromId.bind(this)
submit = async e => { const handleSubmit = () => {
e.preventDefault()
const { pending, curPass, newPass, confPass } = this.state
const { email, _id } = this.props.user
if (pending) return if (pending) return
const doErr = passErr => this.setState({ pending: false, passErr }) let err
const vals = { Object.keys(data).forEach(k => (data[k] = data[k].trim()))
'Current password': curPass, if (!data.current) err = 'current pass is required'
'New password': newPass, else if (!data.new) err = 'new pass is required'
'Confirm new password': confPass, else if (!data.confirm) err = 'confirm pass is required'
} else if (data.new !== data.confirm) err = 'new password must match confirm'
const keys = Object.keys(vals)
for (let i = 0; i < keys.length; i++) {
let key = keys[i],
val = vals[key]
if (val.length === 0) return doErr(`${key} is required`)
}
if (newPass !== confPass) return doErr("New passwords don't match")
this.setState({ passErr: null, pending: true }) if (err) return setError(err)
const updRes = await fetch(getUrl('users/' + _id), { setError(null)
setPending(true)
fetch(addBase('/user'), {
method: 'PATCH', method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: getJwt() }, headers: {
body: JSON.stringify({ email, password: curPass, newPassword: newPass }), ...getHeaders(),
}).catch(doErr) 'content-type': 'application/json',
if (updRes.ok) { },
this.setState({ body: JSON.stringify({
curPass: '', username: user.username,
newPass: '', password: data.current,
confPass: '', newPassword: data.new,
passErr: 'Password updated successfully', }),
pending: false, })
.then(async res => {
const { status, ...data } = await res.json()
if (status === 'ok') {
setPending(false)
setError('Password updated')
return setData({
current: '',
confirm: '',
new: '',
})
}
throw new Error(data.message)
})
.catch(err => {
setPending(false)
setError(err.message || 'An error occurred updating password')
}) })
} else {
let message = 'failed to update password'
try {
const data = await updRes.json()
message = data.message || message
} catch (err) {
doErr(err.message)
}
doErr(message)
}
} }
render() { const handleChange = e => {
const { pending, passErr, curPass, newPass, confPass } = this.state data[e.target.id] = e.target.value
setData(data)
}
return ( return (
<Page> <RequireUser>
<PaddedRow amount={25}> <div className="container padded fill">
<h3>Account settings</h3> <h3>Account settings</h3>
<hr /> <hr />
<form noValidate style={{ padding: '0 0 45px' }}>
<h4>Change password</h4> <h4>Change password</h4>
<fieldset>
<label htmlFor="curPass">Current Password</label> <label htmlFor="current">Current Password</label>
<input <input
type="password" id="current"
id="curPass" type="password"
onChange={this.updVal} placeholder="Current super secret password"
placeholder="Current super secret password..." value={data.current}
value={curPass} onChange={handleChange}
/> />
<label htmlFor="newPass">New Password</label>
<input <label htmlFor="new">New Password</label>
type="password" <input
id="newPass" id="new"
onChange={this.updVal} type="password"
placeholder="New super secret password..." placeholder="New super secret password"
value={newPass} value={data.new}
/> onChange={handleChange}
<label htmlFor="confPass">Confirm New Password</label> />
<input
type="password" <label htmlFor="confirm">Confirm New Password</label>
id="confPass" <input
onChange={this.updVal} id="confirm"
placeholder="Confirm new super secret password..." type="password"
value={confPass} value={data.confirm}
/> onChange={handleChange}
</fieldset> placeholder="Confirm its not too secret you forgot"
<button />
onClick={this.submit}
className={'float-right' + (pending ? ' disabled' : '')} <div>
> {error && <p className="float-left">{error}</p>}
{pending ? <Spinner /> : 'Submit'} <button className="float-right" onClick={handleSubmit}>
</button> Submit
{!passErr ? null : <p>{passErr}</p>} </button>
</form> </div>
</PaddedRow>
</Page> <style jsx>{`
) .container {
} width: 100%;
margin: auto;
padding: 10px;
max-width: 550px;
}
h3 {
margin-top: 15px;
margin-bottom: 0 !important;
}
button {
margin-top: 5px;
}
`}</style>
</div>
</RequireUser>
)
} }
export default connect(mapUser)(Settings)
export default connect(({ user }) => ({ user }))(Settings)

View File

@@ -1,34 +0,0 @@
// Application hooks that run for every service
const logger = require('./hooks/logger')
module.exports = {
before: {
all: [logger()],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: [],
},
after: {
all: [logger()],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: [],
},
error: {
all: [logger()],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: [],
},
}

View File

@@ -1,133 +0,0 @@
const fs = require('fs')
const path = require('path')
const compress = require('compression')
const cors = require('cors')
const helmet = require('helmet')
const cookieParser = require('cookie-parser')()
const RateLimit = require('express-rate-limit')
const trustIPs = require('./trustIPs')
const logger = require('winston')
const feathers = require('@feathersjs/feathers')
const express = require('@feathersjs/express')
const configuration = require('@feathersjs/configuration')
const hostConfig = require('../config/host.json')
const middleware = require('./middleware')
const services = require('./services')
const appHooks = require('./app.hooks')
const channels = require('./channels')
const authentication = require('./authentication')
const dev = process.env.NODE_ENV !== 'production'
const pathPrefix = require('./util/pathPrefix')
const stripBase = require('./util/stripPrefix')
const getUrl = require('./util/getUrl')
const { parse } = require('url')
const Next = require('next')({ dev, quiet: true })
const nextHandler = Next.getRequestHandler()
const app = express(feathers())
global.app = app
app.run = async port => {
const server = app.listen(port)
await Next.prepare()
if (dev) {
server.on('upgrade', (req, socket) => {
req.url = stripBase(req.url)
nextHandler(req, socket, parse(req.url, true))
})
}
return server
}
// Load app configuration
app.configure(configuration())
// load host and setup settings
Object.keys(hostConfig).forEach(key => app.set(key, hostConfig[key]))
app.set('kbConf', {
pathPrefix,
})
app.set('didSetup', false)
try {
fs.statSync(path.join(__dirname, '..', 'db', '.didSetup'))
app.set('didSetup', true)
} catch (err) {
app.use((req, res, next) => {
req.doSetup = !app.get('didSetup')
next()
})
}
const authLimit = new RateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 5, // 5 attempts then block
delayAfter: 3, // slow down after 3 fails
delayMs: 2 * 1000,
})
app.authLimit = authLimit
app.use(getUrl('auth'), authLimit)
app.patch(getUrl('users/*'), authLimit)
// Enable CORS, security, compression, favicon and body parsing
trustIPs(app)
app.use(cors())
app.use(
helmet({
hidePoweredBy: { setTo: 'hamsters' },
})
)
app.use(getUrl('/'), express.static(path.join(__dirname, '../public')))
if (!dev) app.use(compress())
app.use(express.json()) // use { limit } option to increase max post size
app.use(express.urlencoded({ extended: true }))
app.configure(express.rest()) // Set up Plugins and providers
app.configure(middleware) // middleware/index.js
app.configure(authentication) // Set up authentication
app.configure(services) // Set up our services (see `services/index.js`)
app.configure(channels) // Set up event channels (see channels.js)
Next.setAssetPrefix(pathPrefix)
const checkJWT = async (req, res, next) => {
const result = await req.app.authenticate('jwt', {})(req)
if (result.success) {
req.jwt = req.cookies.jwt
delete result.data.user.password
req.user = result.data.user
}
next()
}
;['/', '/logout', '/new', '/settings'].forEach(route => {
app.get(getUrl(route), cookieParser, checkJWT, (req, res) => {
const { query } = parse(req.url, true)
Next.render(req, res, route, query)
})
})
;['/k', '/edit'].forEach(route => {
app.get(getUrl(route + '/:id'), cookieParser, checkJWT, (req, res) => {
Next.render(req, res, route, { id: req.params.id })
})
})
const notFound = express.notFound()
app.use((req, res, next) => {
let accept = req.get('accept')
if (accept && accept.toLowerCase() === 'application/json')
return notFound(req, res, next)
if (req.url.substr(0, pathPrefix.length) !== pathPrefix)
return Next.render404(req, res)
req.url = stripBase(req.url)
nextHandler(req, res, parse(req.url, true))
})
app.use(express.errorHandler({ logger }))
app.hooks(appHooks)
module.exports = app

View File

@@ -1,42 +0,0 @@
const authentication = require('@feathersjs/authentication')
const jwt = require('@feathersjs/authentication-jwt')
const local = require('@feathersjs/authentication-local')
const getUrl = require('./util/getUrl')
module.exports = function(app) {
const config = app.get('authentication')
config.path = getUrl(config.path)
config.service = getUrl('users')
// Set up authentication with the secret
app.configure(
authentication(
Object.assign({}, config, {
cookie: {
enabled: true,
httpOnly: false,
secure: false,
name: 'jwt',
},
})
)
)
app.configure(jwt())
app.configure(local())
// The `authentication` service is used to create a JWT.
// The before `create` hook registers strategies that can be used
// to create a new valid JWT (e.g. local or oauth2)
app.service(config.path).hooks({
before: {
create: [
authentication.hooks.authenticate(config.strategies),
ctx => {
ctx.app.authLimit.resetKey(ctx.params.ip)
return ctx
},
],
remove: [authentication.hooks.authenticate('jwt')],
},
})
}

View File

@@ -1,64 +0,0 @@
module.exports = function(app) {
if (typeof app.channel !== 'function') {
// If no real-time functionality has been configured just return
return
}
app.on('connection', connection => {
// On a new real-time connection, add it to the anonymous channel
app.channel('anonymous').join(connection)
})
app.on('login', (authResult, { connection }) => {
// connection can be undefined if there is no
// real-time connection, e.g. when logging in via REST
if (connection) {
// Obtain the logged in user from the connection
// const user = connection.user;
// The connection is no longer anonymous, remove it
app.channel('anonymous').leave(connection)
// Add it to the authenticated user channel
app.channel('authenticated').join(connection)
// Channels can be named anything and joined on any condition
// E.g. to send real-time events only to admins use
// if(user.isAdmin) { app.channel('admins').join(connection); }
// If the user has joined e.g. chat rooms
// if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel));
// Easily organize users by email and userid for things like messaging
// app.channel(`emails/${user.email}`).join(channel);
// app.channel(`userIds/$(user.id}`).join(channel);
}
})
// eslint-disable-next-line no-unused-vars
app.publish((data, hook) => {
// Here you can add event publishers to channels set up in `channels.js`
// To publish only for a specific event use `app.publish(eventname, () => {})`
// eslint-disable-next-line
console.log(
'Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'
)
// e.g. to publish all service events to all authenticated users use
return app.channel('authenticated')
})
// Here you can also add service specific event publishers
// e..g the publish the `users` service `created` event to the `admins` channel
// app.service('users').publish('created', () => app.channel('admins'));
// With the userid and email organization from above you can easily select involved users
// app.service('messages').publish(() => {
// return [
// app.channel(`userIds/${data.createdBy}`),
// app.channel(`emails/${data.recipientEmail}`)
// ];
// });
}

17
src/client/actionTypes.js Normal file
View File

@@ -0,0 +1,17 @@
import mirror from 'keymirror'
export default mirror({
// user actions
SET_USER: null,
USER_LOGOUT: null,
// docs actions
DOC_DELETED: null,
DOCS_ERROR: null,
DOCS_LOADED: null,
DOCS_PENDING: null,
// cache actions
LOAD_CACHE: null,
CACHE_DOCS: null,
})

View File

@@ -0,0 +1,59 @@
import React, { useEffect } from 'react'
import config from '../../util/pubConfig'
import MonokaiStyles from '../styles/codemirror/monokai'
import CodeMirrorStyles from '../styles/codemirror/codemirror'
let cm
let editor
let textareaRef
if (!config.ssr) {
require('codemirror/mode/markdown/markdown')
cm = require('codemirror')
}
export default function CodeMirror({
value,
style,
className,
options = {},
onChange = () => {},
onSubmit = () => {},
}) {
const handleChange = (cm, e) => onChange(cm.getValue())
const handleSubmit = (cm, e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
onSubmit()
}
}
useEffect(() => {
if (cm) {
if (!editor || editor.getTextArea() !== textareaRef) {
editor = cm.fromTextArea(textareaRef, options)
}
editor.on('change', handleChange)
editor.on('keydown', handleSubmit)
}
return () => {
if (editor) {
editor.off('change', handleChange)
editor.off('keydown', handleSubmit)
}
}
})
return (
<div {...{ className, style }}>
<textarea
{...{
defaultValue: value,
ref: el => (textareaRef = el),
}}
/>
<MonokaiStyles />
<CodeMirrorStyles />
</div>
)
}

104
src/client/comps/editDoc.js Normal file
View File

@@ -0,0 +1,104 @@
import React, { useState } from 'react'
import dynamic from 'next/dynamic'
import { connect } from 'react-redux'
import { updateDoc } from '../util/docHelpers'
import isOkDirPart from '../../util/isOkDirPart'
const Markdown = dynamic(() => import('react-markdown'))
const CodeMirror = dynamic(() => import('./codemirror'))
const dirError =
'contains an invalid character, must only have a-z, 0-9, -, _, and not start or end with a period or space'
function EditDoc({ cache, query }) {
const doc = cache[query.id] || {}
const [md, setMd] = useState(
doc.md || '### New document\n\nHeres some starting text'
)
const [dir, setDir] = useState(doc.dir || '')
const [name, setName] = useState(doc.name || '')
const [error, setError] = useState(null)
const [pending, setPending] = useState(false)
const handleSubmit = () => {
if (pending) return
let err
if (!name.trim()) {
err = 'Name is required'
} else if (!isOkDirPart(name)) {
err = `Name ${dirError}`
} else if (dir && dir.split('/').some(dirPart => !isOkDirPart(dirPart))) {
err = `Directory ${dirError}`
} else if (!md.trim()) {
err = 'Contents of markdown can not be empty'
}
if (err) return setError(err)
setError(null)
setPending(true)
updateDoc(query.id, md, name.trim(), dir).catch(err => {
setError(err.message)
setPending(false)
})
}
return (
<div className="container padded editDoc">
<div className="row">
<div className="column column-50">
<Markdown source={md} className="Markdown" />
</div>
<div className="column column-50">
<div className="row">
<div className="column column-60">
<input
type="text"
value={name}
maxLength={255}
placeholder="Document name"
onChange={e => setName(e.target.value)}
/>
</div>
<div className="column column">
<input
type="text"
value={dir}
placeholder="Directory (optional)"
onChange={e => setDir(e.target.value)}
/>
</div>
</div>
<div className="row">
<CodeMirror
value={md}
onChange={setMd}
onSubmit={handleSubmit}
style={{ width: '100%' }}
className="column wrapCodeMirror"
options={{
theme: 'monokai',
mode: 'markdown',
lineWrapping: true,
}}
/>
</div>
<div className="row" style={{ paddingTop: 15 }}>
<div className="column">
{error && <p className="float-left">{error}</p>}
<button className="float-right" onClick={handleSubmit}>
{pending ? 'Pending' : 'Submit'}
</button>
</div>
</div>
</div>
</div>
<style jsx global>{`
.wrapCodeMirror textarea {
margin-bottom: 0;
min-height: 300px;
}
`}</style>
</div>
)
}
export default connect(({ cache }) => ({ cache }))(EditDoc)

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function extLink({ children, ...props }) {
return (
<a rel="noopener noreferrer" target="_blank" {...props}>
{children}
</a>
)
}

View File

@@ -0,0 +1,172 @@
import React, { useEffect, useState } from 'react'
import config from '../../util/pubConfig'
import addBase from '../../util/addBase'
import loadDocs from '../util/loadDocs'
import { connect } from 'react-redux'
import Paginate from 'react-paginate'
import Router from 'next/router'
import Link from 'next/link'
let searchTimeout
let abortController
const { date, defDocsLimit, searchDelay, ssr } = config
const abort = () => {
abortController && abortController.abort()
}
function ListDocs({ docs, query }) {
const [offline, setOffline] = useState(ssr ? false : !navigator.onLine)
const curSort = query.sort || 'updated:-1'
const curPage = parseInt(query.page, 10) || 1
const pageCount = Math.ceil(docs.total / defDocsLimit)
const handleOffline = () => setOffline(true)
const handleOnline = () => setOffline(false)
useEffect(
() => {
abortController = loadDocs(query)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
abort()
clearTimeout(searchTimeout)
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
},
[query]
)
const updateUrl = query => {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => {
Router.push({ pathname: '/', query }, { pathname: addBase('/'), query })
}, searchDelay)
}
const handleField = e => {
const key = e.target.getAttribute('name')
updateUrl({
...query,
page: 1,
[key]: e.target.value.trim(),
})
}
const handlePage = ({ selected }) => {
if (curPage - 1 === selected) return
updateUrl({
...query,
page: selected + 1,
})
}
return (
<div className="container padded">
<form action={addBase('/')} method="GET">
<div className="row">
<input
type="text"
name="search"
maxLength={300}
className="search"
onChange={handleField}
defaultValue={query.search || ''}
placeholder="Search knowledge base..."
/>
<select
name="sort"
value={curSort}
onChange={handleField}
className="column column-25"
>
<option value="updated:-1">Updated (new to old)</option>
<option value="updated:1">Updated (old to new)</option>
<option value="created:-1">Created (new to old)</option>
<option value="created:1">Created (old to new)</option>
<option value="id:1">Path (A to Z)</option>
<option value="id:-1">Path (Z to A)</option>
</select>
</div>
<noscript>
<button type="submit" className="float-right">
Submit
</button>
</noscript>
</form>
{docs.error && <p>{docs.error} </p>}
<table>
<thead>
<tr>
<th>Doc{offline && ` (offline mode)`}</th>
<th>Modified</th>
</tr>
</thead>
<tbody>
{docs.results.map(doc => {
const docUrl = { pathname: '/doc', query: { id: doc.id } }
return (
<tr key={doc.id}>
<td>
<Link
href={docUrl}
as={{ ...docUrl, pathname: addBase('/doc') }}
>
<a>{doc.id}</a>
</Link>
</td>
<td>
{new Date(doc.updated).toLocaleDateString(
date.locale,
date.options
)}
</td>
</tr>
)
})}
</tbody>
</table>
{!docs.total && <p>No docs found...</p>}
{docs.total > defDocsLimit && (
<Paginate
previousLabel="Prev"
pageCount={pageCount}
marginPagesDisplayed={2}
activeClassName="active"
forcePage={curPage - 1}
onPageChange={handlePage}
containerClassName="paginate"
hrefBuilder={pg => addBase(`/?page=${pg}`)}
/>
)}
<style jsx>{`
.container {
max-width: 750px;
margin: 15px auto;
}
.row input {
margin-right: 20px;
}
td a {
display: block;
}
th:nth-of-type(2n),
td:nth-of-type(2n) {
text-align: right;
}
`}</style>
</div>
)
}
export default connect(({ docs }) => ({ docs }))(ListDocs)

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { connect } from 'react-redux'
import Login from '../forms/login'
function RequireUser({ children, user }) {
return user.id ? children : <Login doSetup={user.doSetup} />
}
export default connect(({ user }) => ({ user }))(RequireUser)

View File

@@ -0,0 +1,72 @@
import { useEffect } from 'react'
import Router from 'next/router'
import logout from '../util/logout'
import addBase from '../../util/addBase'
/* - keyboard shortcuts
g then h -> navigate home
g then n -> navigate to new doc
g then s -> navigate to settings
g then l -> logout
e (when on doc page) -> edit doc
/ (when on home page) -> focus search
ctrl/cmd + enter -> submit new doc (handled in CodeMirror component)
*/
const keyToUrl = {
72: '/',
78: '/new',
83: '/settings',
}
const keyToEl = {
69: { sel: '#edit', func: 'click' },
191: { sel: '.search', func: 'focus' },
}
const getKey = e => e.which || e.keyCode
let prevKey
const handleKeyDown = e => {
const tag = e.target.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA') return
const key = getKey(e)
if (prevKey === 71) {
// prev key was g
switch (key) {
case 72:
case 78:
case 83: {
const url = keyToUrl[key]
Router.push(url, addBase(url))
break
}
case 76: {
// logout
setTimeout(logout, 1)
break
}
default:
break
}
}
switch (key) {
case 69:
case 191: {
const { sel, func } = keyToEl[key]
const el = document.querySelector(sel)
if (el) setTimeout(() => el[func](), 1)
break
}
default:
break
}
prevKey = key
}
export default function Shortcuts(props) {
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
return null
}

112
src/client/forms/login.js Normal file
View File

@@ -0,0 +1,112 @@
import React, { useState, useEffect } from 'react'
import addBase from '../../util/addBase'
import actionTypes from '../actionTypes'
import { getStore } from '../store'
export default function Login({ doSetup }) {
const [mounted, setMounted] = useState(false)
const [pending, setPending] = useState(false)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const endpoint = addBase(doSetup ? '/register' : '/auth')
useEffect(() => setMounted(true), [])
const submit = e => {
e.preventDefault()
if (pending || !username || !password) return
setPending(true)
setError(null)
fetch(endpoint, {
method: 'POST',
credentials: 'include',
body: JSON.stringify({
username,
password,
}),
headers: {
'content-type': 'application/json',
},
})
.then(res => {
if (res.status === 429) {
throw new Error('Too many login attempts')
}
return res.json()
})
.then(({ accessToken, message }) => {
if (message) {
throw new Error(message)
}
localStorage.setItem('jwt', accessToken)
getStore().dispatch({
type: actionTypes.SET_USER,
user: {
...JSON.parse(atob(accessToken.split('.')[1])).user,
jwt: accessToken,
},
})
})
.catch(err => {
setError(err.message)
setPending(false)
})
}
return (
<div className="fill">
<h4>{doSetup ? 'Setup account' : 'Please login to continue'}</h4>
<form action={endpoint} method="post">
<label>
Username:
<input
required
autoFocus
type="text"
name="username"
value={username}
placeholder="Username"
onChange={e => setUsername(e.target.value.trim())}
/>
</label>
<label>
Password:
<input
required
type="password"
name="password"
value={password}
placeholder="Super secret password"
onChange={e => setPassword(e.target.value.trim())}
/>
</label>
{!mounted && <input type="hidden" value="true" name="form" />}
{error && <p className="float-left">{error}</p>}
<button disabled={pending} onClick={submit}>
{pending ? 'Pending...' : 'SUBMIT'}
</button>
</form>
<style jsx>{`
div {
width: 100%;
margin: auto;
padding: 10px;
max-width: 550px;
justify-content: center;
}
input {
margin-top: 5px;
}
button {
float: right;
}
`}</style>
</div>
)
}

View File

@@ -0,0 +1,58 @@
import React, { Component } from 'react'
import { initializeStore } from '../store'
import checkLogin from '../util/checkLogin'
import config from '../../util/pubConfig'
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'
function getOrCreateStore(initialState) {
// Always make a new store if server, otherwise state is shared between requests
if (config.ssr) {
global.reduxStore = initializeStore(initialState)
return global.reduxStore
}
// Create store if unavailable on the client and set it on the window object
if (!window[__NEXT_REDUX_STORE__]) {
window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
}
return window[__NEXT_REDUX_STORE__]
}
export default App => {
return class AppWithRedux extends Component {
static async getInitialProps(appContext) {
// Get or Create the store with `undefined` as initialState
// This allows you to set a custom default initialState
const reduxStore = getOrCreateStore()
// Provide the store to getInitialProps of pages
appContext.ctx.reduxStore = reduxStore
await checkLogin(appContext.ctx.req)
let appProps = {}
if (typeof App.getInitialProps === 'function') {
appProps = await App.getInitialProps.call(App, appContext)
}
return {
...appProps,
initialReduxState: reduxStore.getState(),
}
}
constructor(props) {
super(props)
this.reduxStore = getOrCreateStore(props.initialReduxState)
}
componentDidMount() {
checkLogin()
}
render() {
return <App {...this.props} reduxStore={this.reduxStore} />
}
}
}

View File

@@ -0,0 +1,30 @@
import React from 'react'
import theme from '../theme'
import ExtLink from '../comps/extLink'
export default function Footer(props) {
return (
<footer>
<p>
<span>Powered by</span>
<ExtLink href="//github.com/ijjk/mykb">MYKB</ExtLink>
</p>
<style jsx>{`
footer {
text-align: center;
background: ${theme.primaryAlt};
}
p {
padding: 10px 10px 15px;
margin-bottom: 0;
}
span {
padding-right: 5px;
}
`}</style>
</footer>
)
}

164
src/client/layout/header.js Normal file
View File

@@ -0,0 +1,164 @@
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import theme from '../theme'
import logout from '../util/logout'
import { connect } from 'react-redux'
import { withRouter } from 'next/router'
import addBase from '../../util/addBase'
const navLinks = [
{ link: '/', label: 'Home' },
{ link: '/new', label: 'New' },
{ link: '/settings', label: 'Settings' },
]
function Header({ user, router }) {
const curPath = addBase(router.pathname)
const [menuOpen, setOpen] = useState(false)
const handleChange = () => setOpen(false)
useEffect(() => {
router.events.on('routeChangeComplete', handleChange)
return () => router.events.off('routeChangeComplete', handleChange)
}, [])
return (
<header>
<h3>
<Link href="/" as={addBase('/')}>
<a>MYKB</a>
</Link>
</h3>
{user.id && (
<>
<label htmlFor="menu">MENU</label>
<input
id="menu"
type="checkbox"
checked={menuOpen}
onChange={e => setOpen(e.target.checked)}
/>
</>
)}
<nav>
{user.id && (
<ul>
{navLinks.map(({ link, label }) => (
<li key={label}>
<Link href={link} as={addBase(link)}>
<a className={addBase(link) === curPath ? 'active' : null}>
{label}
</a>
</Link>
</li>
))}
<li>
<a href={addBase('/logout')} onClick={logout}>
Logout
</a>
</li>
</ul>
)}
</nav>
<style jsx>{`
header {
height: 55px;
display: flex;
padding: 0 10px;
flex-direction: row;
align-items: center;
background: ${theme.primaryAlt};
}
h3 {
margin: 0;
line-height: 1;
}
label {
cursor: pointer;
margin-left: auto;
user-select: none;
color: ${theme.link};
}
input[type='checkbox'] {
display: none;
}
nav {
left: 0;
top: 55px;
height: 0;
width: 100%;
z-index: 10;
position: fixed;
overflow: hidden;
background: ${theme.primaryAlt};
transition: height 200ms ease-in-out;
}
input:checked ~ nav {
height: 180px;
}
ul {
margin: 0;
list-style: none;
}
li {
margin: 0;
}
li a {
width: 100vw;
display: flex;
padding: 10px;
align-items: center;
justify-content: center;
transition: all ease 200ms;
}
li a.active,
li a:hover {
opacity: 1;
background: ${theme.primary};
}
@media (min-width: 420px) {
label {
display: none;
}
nav,
ul {
top: 0;
left: none;
height: 55px;
width: auto;
flex-grow: 1;
position: relative;
display: inline-flex;
flex-direction: row;
justify-content: flex-end;
}
li {
margin: 0;
}
li a {
height: 55px;
width: initial;
padding: 0 15px;
}
}
`}</style>
</header>
)
}
export default withRouter(connect(({ user }) => ({ user }))(Header))

51
src/client/store.js Normal file
View File

@@ -0,0 +1,51 @@
import { combineReducers, compose, createStore } from 'redux'
import config from '../util/pubConfig'
import actionTypes from './actionTypes'
// import stores
import user from './stores/userStore'
import docs from './stores/docsStore'
import cache from './stores/cacheStore'
export function initializeStore(initialState) {
let enhancer = undefined
if (!config.ssr) {
const persistState = require('redux-localstorage')
enhancer = compose(
persistState(['cache', 'user'], {
merge: (initial, persisted) => {
return {
...initial,
cache: {
...persisted.cache,
...initial.cache,
},
user: {
...initial.user,
...persisted.user,
},
}
},
})
)
}
const store = combineReducers({
user,
docs,
cache,
})
const rootStore = (state, action) => {
if (action.type === actionTypes.USER_LOGOUT) {
state = undefined
} else if (action.type === actionTypes.LOAD_CACHE) {
state = action.state
}
return store(state, action)
}
return createStore(rootStore, initialState, enhancer)
}
export function getStore() {
return config.ssr ? global.reduxStore : window['__NEXT_REDUX_STORE__']
}

View File

@@ -0,0 +1,27 @@
import actionTypes from '../actionTypes'
const initialState = {}
export default function cache(state = initialState, action) {
switch (action.type) {
case actionTypes.DOCS_LOADED:
case actionTypes.CACHE_DOCS: {
if (!action.data || !action.data.results) return state
// update cache with new results
action.data.results.forEach(doc => {
state[doc.id] = doc
})
return { ...state }
}
case actionTypes.DOC_DELETED: {
return {
...state,
[action.id]: undefined,
}
}
default:
return state
}
}

View File

@@ -0,0 +1,51 @@
import actionTypes from '../actionTypes'
const initialState = {
hasMore: false,
pending: false,
fetchIdx: 0,
error: null,
total: null,
results: [],
page: 1,
}
export default function docs(state = initialState, action) {
if (
action.type !== actionTypes.DOCS_PENDING &&
action.fetchIdx !== state.fetchIdx
) {
return state
}
switch (action.type) {
case actionTypes.DOCS_PENDING: {
return {
...state,
error: null,
pending: true,
fetchIdx: action.fetchIdx,
}
}
case actionTypes.DOCS_LOADED: {
return {
...state,
...action.data,
pending: false,
}
}
case actionTypes.DOCS_ERROR: {
return {
...state,
pending: false,
error: action.error,
}
}
default: {
return state
}
}
}

View File

@@ -0,0 +1,28 @@
import actionTypes from '../actionTypes'
import config from '../../util/pubConfig'
const initialState = {
id: null,
email: null,
admin: null,
doSetup: false,
verified: false,
}
export default function user(state = initialState, action) {
switch (action.type) {
case actionTypes.SET_USER: {
if (state.doSetup && !action.user.doSetup) {
config.doSetup = false
}
return {
...initialState,
...action.user,
}
}
default: {
return state
}
}
}

View File

@@ -0,0 +1,488 @@
import React from 'react'
export default function codemirror(props) {
return (
<style jsx global>{`
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler,
.CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {
}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker {
color: black;
}
.CodeMirror-guttermarker-subtle {
color: #999;
}
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {
}
50% {
background-color: transparent;
}
100% {
}
}
@-webkit-keyframes blink {
0% {
}
50% {
background-color: transparent;
}
100% {
}
}
@keyframes blink {
0% {
}
50% {
background-color: transparent;
}
100% {
}
}
.cm-tab {
display: inline-block;
text-decoration: inherit;
}
.CodeMirror-rulers {
position: absolute;
left: 0;
right: 0;
top: -50px;
bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0;
bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {
color: blue;
}
.cm-s-default .cm-quote {
color: #090;
}
.cm-negative {
color: #d44;
}
.cm-positive {
color: #292;
}
.cm-header,
.cm-strong {
font-weight: bold;
}
.cm-em {
font-style: italic;
}
.cm-link {
text-decoration: underline;
}
.cm-strikethrough {
text-decoration: line-through;
}
.cm-s-default .cm-keyword {
color: #708;
}
.cm-s-default .cm-atom {
color: #219;
}
.cm-s-default .cm-number {
color: #164;
}
.cm-s-default .cm-def {
color: #00f;
}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {
}
.cm-s-default .cm-variable-2 {
color: #05a;
}
.cm-s-default .cm-variable-3,
.cm-s-default .cm-type {
color: #085;
}
.cm-s-default .cm-comment {
color: #a50;
}
.cm-s-default .cm-string {
color: #a11;
}
.cm-s-default .cm-string-2 {
color: #f50;
}
.cm-s-default .cm-meta {
color: #555;
}
.cm-s-default .cm-qualifier {
color: #555;
}
.cm-s-default .cm-builtin {
color: #30a;
}
.cm-s-default .cm-bracket {
color: #997;
}
.cm-s-default .cm-tag {
color: #170;
}
.cm-s-default .cm-attribute {
color: #00c;
}
.cm-s-default .cm-hr {
color: #999;
}
.cm-s-default .cm-link {
color: #00c;
}
.cm-s-default .cm-error {
color: #f00;
}
.cm-invalidchar {
color: #f00;
}
.CodeMirror-composing {
border-bottom: 2px solid;
}
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {
color: #0b0;
}
div.CodeMirror span.CodeMirror-nonmatchingbracket {
color: #a22;
}
.CodeMirror-matchingtag {
background: rgba(255, 150, 0, 0.3);
}
.CodeMirror-activeline-background {
background: #e8f2ff;
}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px;
margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar,
.CodeMirror-hscrollbar,
.CodeMirror-scrollbar-filler,
.CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0;
top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0;
left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0;
bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0;
bottom: 0;
}
.CodeMirror-gutters {
position: absolute;
left: 0;
top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0;
bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection {
background-color: transparent;
}
.CodeMirror-gutter-wrapper ::-moz-selection {
background-color: transparent;
}
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {
}
.CodeMirror-rtl pre {
direction: rtl;
}
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre {
position: static;
}
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected {
background: #d9d9d9;
}
.CodeMirror-focused .CodeMirror-selected {
background: #d7d4f0;
}
.CodeMirror-crosshair {
cursor: crosshair;
}
.CodeMirror-line::selection,
.CodeMirror-line > span::selection,
.CodeMirror-line > span > span::selection {
background: #d7d4f0;
}
.CodeMirror-line::-moz-selection,
.CodeMirror-line > span::-moz-selection,
.CodeMirror-line > span > span::-moz-selection {
background: #d7d4f0;
}
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, 0.4);
}
/* Used to force a border model for a node */
.cm-force-border {
padding-right: 0.1px;
}
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after {
content: '';
}
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext {
background: none;
}
`}</style>
)
}

View File

@@ -0,0 +1,117 @@
import React from 'react'
export default function monokai(props) {
return (
<style jsx global>{`
/* Based on Sublime Text's Monokai theme */
.cm-s-monokai.CodeMirror {
background: #272822;
color: #f8f8f2;
}
.cm-s-monokai div.CodeMirror-selected {
background: #49483e;
}
.cm-s-monokai .CodeMirror-line::selection,
.cm-s-monokai .CodeMirror-line > span::selection,
.cm-s-monokai .CodeMirror-line > span > span::selection {
background: rgba(73, 72, 62, 0.99);
}
.cm-s-monokai .CodeMirror-line::-moz-selection,
.cm-s-monokai .CodeMirror-line > span::-moz-selection,
.cm-s-monokai .CodeMirror-line > span > span::-moz-selection {
background: rgba(73, 72, 62, 0.99);
}
.cm-s-monokai .CodeMirror-gutters {
background: #272822;
border-right: 0px;
}
.cm-s-monokai .CodeMirror-guttermarker {
color: white;
}
.cm-s-monokai .CodeMirror-guttermarker-subtle {
color: #d0d0d0;
}
.cm-s-monokai .CodeMirror-linenumber {
color: #d0d0d0;
}
.cm-s-monokai .CodeMirror-cursor {
border-left: 1px solid #f8f8f0;
}
.cm-s-monokai span.cm-comment {
color: #75715e;
}
.cm-s-monokai span.cm-atom {
color: #ae81ff;
}
.cm-s-monokai span.cm-number {
color: #ae81ff;
}
.cm-s-monokai span.cm-comment.cm-attribute {
color: #97b757;
}
.cm-s-monokai span.cm-comment.cm-def {
color: #bc9262;
}
.cm-s-monokai span.cm-comment.cm-tag {
color: #bc6283;
}
.cm-s-monokai span.cm-comment.cm-type {
color: #5998a6;
}
.cm-s-monokai span.cm-property,
.cm-s-monokai span.cm-attribute {
color: #a6e22e;
}
.cm-s-monokai span.cm-keyword {
color: #f92672;
}
.cm-s-monokai span.cm-builtin {
color: #66d9ef;
}
.cm-s-monokai span.cm-string {
color: #e6db74;
}
.cm-s-monokai span.cm-variable {
color: #f8f8f2;
}
.cm-s-monokai span.cm-variable-2 {
color: #9effff;
}
.cm-s-monokai span.cm-variable-3,
.cm-s-monokai span.cm-type {
color: #66d9ef;
}
.cm-s-monokai span.cm-def {
color: #fd971f;
}
.cm-s-monokai span.cm-bracket {
color: #f8f8f2;
}
.cm-s-monokai span.cm-tag {
color: #f92672;
}
.cm-s-monokai span.cm-header {
color: #ae81ff;
}
.cm-s-monokai span.cm-link {
color: #ae81ff;
}
.cm-s-monokai span.cm-error {
background: #f92672;
color: #f8f8f0;
}
.cm-s-monokai .CodeMirror-activeline-background {
background: #373831;
}
.cm-s-monokai .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}
`}</style>
)
}

138
src/client/styles/global.js Normal file
View File

@@ -0,0 +1,138 @@
import React from 'react'
import theme from '../theme'
export default function global(props) {
return (
<style jsx global>{`
* {
box-sizing: border-box;
}
body, code, pre {
background: ${theme.primary};
color: ${theme.text};
margin: 0;
}
pre, code {
font-size: 1.5rem;
}
input, textarea, select, button, .button, .cm-s-monokai.CodeMirror {
color: ${theme.text};
border-radius: 0.4rem;
border: none !important;
background-color ${theme.primaryAlt} !important;
}
button[disabled] {
cursor: default;
}
input, textarea {
font-size: 1.6rem;
font-family: ${theme.fontFamily};
font-weight: 300;
resize: none;
}
input[disabled], textarea[disabled] {
opacity: 0.8;
cursor: not-allowed;
}
input::placeholder, textarea::placeholder {
opacity: 0.85;
color: ${theme.text};
}
select {
-webkit-appearance: none;
-moz-appearance: none;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%23d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
}
select:focus {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%239b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
}
a {
color: ${theme.link};
cursor: pointer;
}
a:visited {
color: ${theme.link};
}
a:focus, a:hover {
color: ${theme.linkAct};
}
.Markdown pre {
margin-bottom: 2.5rem;
}
.row {
margin: 0 !important;
width: 100% !important;
}
.fill {
/* 55px: header height, 50px: footer height */
display: flex;
flex-direction: column;
min-height: calc(100vh - 55px - 51px);
}
.main > .padded.container {
padding-top: 20px;
padding-bottom: 20px;
}
.danger {
color: ${theme.danger};
}
.paginate {
list-style: none;
text-align: center;
user-select: none;
margin: 0;
}
.paginate li {
display: inline-block;
}
.paginate li.active a {
border-color: ${theme.link};
}
.paginate a {
outline: 0;
border-radius: 50%;
border: 1px solid;
border-color: transparent;
padding: 3px 8px;
}
@media (max-width: 840px) {
.row .column.column-10,
.row .column.column-20,
.row .column.column-25,
.row .column.column-33,
.row .column.column-40,
.row .column.column-50,
.row .column.column-60,
.row .column.column-67,
.row .column.column-75,
.row .column.column-80,
.row .column.column-90 {
flex: 1 1 auto !important;
max-width: 100% !important;
}
}
`}</style>
)
}

View File

@@ -0,0 +1,539 @@
import React from 'react'
export default function milligram(props) {
return (
<style jsx global>{`
*,
*:after,
*:before {
box-sizing: inherit;
}
html {
box-sizing: border-box;
font-size: 62.5%;
}
body {
color: #606c76;
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial',
sans-serif;
font-size: 1.6em;
font-weight: 300;
letter-spacing: 0.01em;
line-height: 1.6;
}
blockquote {
border-left: 0.3rem solid #d1d1d1;
margin-left: 0;
margin-right: 0;
padding: 1rem 1.5rem;
}
blockquote *:last-child {
margin-bottom: 0;
}
.button,
button,
input[type='button'],
input[type='reset'],
input[type='submit'] {
background-color: #9b4dca;
border: 0.1rem solid #9b4dca;
border-radius: 0.4rem;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 1.1rem;
font-weight: 700;
height: 3.8rem;
letter-spacing: 0.1rem;
line-height: 3.8rem;
padding: 0 3rem;
text-align: center;
text-decoration: none;
text-transform: uppercase;
white-space: nowrap;
}
.button:focus,
.button:hover,
button:focus,
button:hover,
input[type='button']:focus,
input[type='button']:hover,
input[type='reset']:focus,
input[type='reset']:hover,
input[type='submit']:focus,
input[type='submit']:hover {
background-color: #606c76;
border-color: #606c76;
color: #fff;
outline: 0;
}
.button[disabled],
button[disabled],
input[type='button'][disabled],
input[type='reset'][disabled],
input[type='submit'][disabled] {
cursor: default;
opacity: 0.5;
}
.button[disabled]:focus,
.button[disabled]:hover,
button[disabled]:focus,
button[disabled]:hover,
input[type='button'][disabled]:focus,
input[type='button'][disabled]:hover,
input[type='reset'][disabled]:focus,
input[type='reset'][disabled]:hover,
input[type='submit'][disabled]:focus,
input[type='submit'][disabled]:hover {
background-color: #9b4dca;
border-color: #9b4dca;
}
.button.button-outline,
button.button-outline,
input[type='button'].button-outline,
input[type='reset'].button-outline,
input[type='submit'].button-outline {
background-color: transparent;
color: #9b4dca;
}
.button.button-outline:focus,
.button.button-outline:hover,
button.button-outline:focus,
button.button-outline:hover,
input[type='button'].button-outline:focus,
input[type='button'].button-outline:hover,
input[type='reset'].button-outline:focus,
input[type='reset'].button-outline:hover,
input[type='submit'].button-outline:focus,
input[type='submit'].button-outline:hover {
background-color: transparent;
border-color: #606c76;
color: #606c76;
}
.button.button-outline[disabled]:focus,
.button.button-outline[disabled]:hover,
button.button-outline[disabled]:focus,
button.button-outline[disabled]:hover,
input[type='button'].button-outline[disabled]:focus,
input[type='button'].button-outline[disabled]:hover,
input[type='reset'].button-outline[disabled]:focus,
input[type='reset'].button-outline[disabled]:hover,
input[type='submit'].button-outline[disabled]:focus,
input[type='submit'].button-outline[disabled]:hover {
border-color: inherit;
color: #9b4dca;
}
.button.button-clear,
button.button-clear,
input[type='button'].button-clear,
input[type='reset'].button-clear,
input[type='submit'].button-clear {
background-color: transparent;
border-color: transparent;
color: #9b4dca;
}
.button.button-clear:focus,
.button.button-clear:hover,
button.button-clear:focus,
button.button-clear:hover,
input[type='button'].button-clear:focus,
input[type='button'].button-clear:hover,
input[type='reset'].button-clear:focus,
input[type='reset'].button-clear:hover,
input[type='submit'].button-clear:focus,
input[type='submit'].button-clear:hover {
background-color: transparent;
border-color: transparent;
color: #606c76;
}
.button.button-clear[disabled]:focus,
.button.button-clear[disabled]:hover,
button.button-clear[disabled]:focus,
button.button-clear[disabled]:hover,
input[type='button'].button-clear[disabled]:focus,
input[type='button'].button-clear[disabled]:hover,
input[type='reset'].button-clear[disabled]:focus,
input[type='reset'].button-clear[disabled]:hover,
input[type='submit'].button-clear[disabled]:focus,
input[type='submit'].button-clear[disabled]:hover {
color: #9b4dca;
}
code {
background: #f4f5f6;
border-radius: 0.4rem;
font-size: 86%;
margin: 0 0.2rem;
padding: 0.2rem 0.5rem;
white-space: nowrap;
}
pre {
background: #f4f5f6;
border-left: 0.3rem solid #9b4dca;
overflow-y: hidden;
}
pre > code {
border-radius: 0;
display: block;
padding: 1rem 1.5rem;
white-space: pre;
}
hr {
border: 0;
border-top: 0.1rem solid #f4f5f6;
margin: 3rem 0;
}
input[type='email'],
input[type='number'],
input[type='password'],
input[type='search'],
input[type='tel'],
input[type='text'],
input[type='url'],
input[type='color'],
input[type='date'],
input[type='month'],
input[type='week'],
input[type='datetime'],
input[type='datetime-local'],
input:not([type]),
textarea,
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
border: 0.1rem solid #d1d1d1;
border-radius: 0.4rem;
box-shadow: none;
box-sizing: inherit;
height: 3.8rem;
padding: 0.6rem 1rem;
width: 100%;
}
input[type='email']:focus,
input[type='number']:focus,
input[type='password']:focus,
input[type='search']:focus,
input[type='tel']:focus,
input[type='text']:focus,
input[type='url']:focus,
input[type='color']:focus,
input[type='date']:focus,
input[type='month']:focus,
input[type='week']:focus,
input[type='datetime']:focus,
input[type='datetime-local']:focus,
input:not([type]):focus,
textarea:focus,
select:focus {
border-color: #9b4dca;
outline: 0;
}
select {
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%23d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>')
center right no-repeat;
padding-right: 3rem;
}
select:focus {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="%239b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
}
textarea {
min-height: 6.5rem;
}
label,
legend {
display: block;
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
fieldset {
border-width: 0;
padding: 0;
}
input[type='checkbox'],
input[type='radio'] {
display: inline;
}
.label-inline {
display: inline-block;
font-weight: normal;
margin-left: 0.5rem;
}
.container {
margin: 0 auto;
max-width: 112rem;
padding: 0 2rem;
position: relative;
width: 100%;
}
.row {
display: flex;
flex-direction: column;
padding: 0;
width: 100%;
}
.row.row-no-padding {
padding: 0;
}
.row.row-no-padding > .column {
padding: 0;
}
.row.row-wrap {
flex-wrap: wrap;
}
.row.row-top {
align-items: flex-start;
}
.row.row-bottom {
align-items: flex-end;
}
.row.row-center {
align-items: center;
}
.row.row-stretch {
align-items: stretch;
}
.row.row-baseline {
align-items: baseline;
}
.row .column {
display: block;
flex: 1 1 auto;
margin-left: 0;
max-width: 100%;
width: 100%;
}
.row .column.column-offset-10 {
margin-left: 10%;
}
.row .column.column-offset-20 {
margin-left: 20%;
}
.row .column.column-offset-25 {
margin-left: 25%;
}
.row .column.column-offset-33,
.row .column.column-offset-34 {
margin-left: 33.3333%;
}
.row .column.column-offset-50 {
margin-left: 50%;
}
.row .column.column-offset-66,
.row .column.column-offset-67 {
margin-left: 66.6666%;
}
.row .column.column-offset-75 {
margin-left: 75%;
}
.row .column.column-offset-80 {
margin-left: 80%;
}
.row .column.column-offset-90 {
margin-left: 90%;
}
.row .column.column-10 {
flex: 0 0 10%;
max-width: 10%;
}
.row .column.column-20 {
flex: 0 0 20%;
max-width: 20%;
}
.row .column.column-25 {
flex: 0 0 25%;
max-width: 25%;
}
.row .column.column-33,
.row .column.column-34 {
flex: 0 0 33.3333%;
max-width: 33.3333%;
}
.row .column.column-40 {
flex: 0 0 40%;
max-width: 40%;
}
.row .column.column-50 {
flex: 0 0 50%;
max-width: 50%;
}
.row .column.column-60 {
flex: 0 0 60%;
max-width: 60%;
}
.row .column.column-66,
.row .column.column-67 {
flex: 0 0 66.6666%;
max-width: 66.6666%;
}
.row .column.column-75 {
flex: 0 0 75%;
max-width: 75%;
}
.row .column.column-80 {
flex: 0 0 80%;
max-width: 80%;
}
.row .column.column-90 {
flex: 0 0 90%;
max-width: 90%;
}
.row .column .column-top {
align-self: flex-start;
}
.row .column .column-bottom {
align-self: flex-end;
}
.row .column .column-center {
-ms-grid-row-align: center;
align-self: center;
}
@media (min-width: 40rem) {
.row {
flex-direction: row;
margin-left: -1rem;
width: calc(100% + 2rem);
}
.row .column {
margin-bottom: inherit;
padding: 0 1rem;
}
}
a {
color: #9b4dca;
text-decoration: none;
}
a:focus,
a:hover {
color: #606c76;
}
dl,
ol,
ul {
list-style: none;
margin-top: 0;
padding-left: 0;
}
dl dl,
dl ol,
dl ul,
ol dl,
ol ol,
ol ul,
ul dl,
ul ol,
ul ul {
font-size: 90%;
margin: 1.5rem 0 1.5rem 3rem;
}
ol {
list-style: decimal inside;
}
ul {
list-style: circle inside;
}
.button,
button,
dd,
dt,
li {
margin-bottom: 1rem;
}
fieldset,
input,
select,
textarea {
margin-bottom: 1.5rem;
}
blockquote,
dl,
figure,
form,
ol,
p,
pre,
table,
ul {
margin-bottom: 2.5rem;
}
table {
border-spacing: 0;
width: 100%;
}
td,
th {
border-bottom: 0.1rem solid #e1e1e1;
padding: 1.2rem 1.5rem;
text-align: left;
}
td:first-child,
th:first-child {
padding-left: 0;
}
td:last-child,
th:last-child {
padding-right: 0;
}
b,
strong {
font-weight: bold;
}
p {
margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 300;
letter-spacing: -0.1rem;
margin-bottom: 2rem;
margin-top: 0;
}
h1 {
font-size: 4.6rem;
line-height: 1.2;
}
h2 {
font-size: 3.6rem;
line-height: 1.25;
}
h3 {
font-size: 2.8rem;
line-height: 1.3;
}
h4 {
font-size: 2.2rem;
letter-spacing: -0.08rem;
line-height: 1.35;
}
h5 {
font-size: 1.8rem;
letter-spacing: -0.05rem;
line-height: 1.5;
}
h6 {
font-size: 1.6rem;
letter-spacing: 0;
line-height: 1.4;
}
img {
max-width: 100%;
}
.clearfix:after {
clear: both;
content: ' ';
display: table;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
`}</style>
)
}

304
src/client/styles/roboto.js Normal file
View File

@@ -0,0 +1,304 @@
import React from 'react'
export default function roboto(props) {
return (
<style jsx global>{`
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc3CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc-CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc2CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc5CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc1CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc0CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TjASc6CsTYl4BO.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic3CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic-CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic2CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic5CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic1CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic0CsTYl4BOQ3o.woff2)
format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOjCnqEu92Fr1Mu51TzBic6CsTYl4BO.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCRc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fABc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCBc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBxc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fCxc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fChc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmSU5fBBc4AMP6lQ.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCRc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF,
U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfABc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCBc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfBxc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfCxc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfChc4AMP6lbBP.woff2)
format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url(https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmWUlfBBc4AMP6lQ.woff2)
format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193,
U+2212, U+2215, U+FEFF, U+FFFD;
}
`}</style>
)
}

View File

@@ -1,9 +1,9 @@
export default { export default {
primary: '#202225', primary: '#202225',
primaryAlt: '#2c2f33', primaryAlt: '#2c2f33',
danger: '#d44848', danger: '#d44848',
text: '#dcddde', text: '#dcddde',
link: '#00d1b2', link: '#00d1b2',
linkAct: '#009e87', linkAct: '#009e87',
fontFamily: `'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif`, fontFamily: `'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif`,
} }

View File

@@ -0,0 +1,50 @@
import { getStore } from '../store'
import actionTypes from '../actionTypes'
import addBase from '../../util/addBase'
import config from '../../util/pubConfig'
export default async function checkLogin(req) {
let user = req && req.user
if (user) {
user = {
...user,
jwt: req.cookies.jwt,
}
}
const store = getStore()
if (!user && !config.ssr && !store.getState().user.verified) {
const jwt = localStorage.jwt
if (jwt) {
const jwt = localStorage.jwt
await fetch(addBase('/auth'), {
headers: {
Authorization: `bearer ${jwt}`,
},
method: 'POST',
})
.then(res => {
if (res.ok) {
const payload = JSON.parse(atob(jwt.split('.')[1]))
user = {
...payload.user,
verified: true,
jwt,
}
}
})
.catch(() => {})
}
}
if (!user && config.doSetup) {
user = { doSetup: true }
}
if (user)
store.dispatch({
type: actionTypes.SET_USER,
user,
})
}

View File

@@ -0,0 +1,63 @@
import { getStore } from '../store'
import Router from 'next/router'
import getHeaders from './getHeaders'
import actionTypes from '../actionTypes'
import addBase from '../../util/addBase'
/**
* delete doc
* @param { String } id - id of doc to delete
* @returns { Promise }
*/
export const deleteDoc = (id, confirm = true) => {
if (confirm && !window.confirm('Are you sure you want to delete this doc?')) {
return
}
return fetch(addBase(`/docs?id=${id}`), {
headers: getHeaders(),
method: 'DELETE',
})
.then(() => {
Router.push('/', addBase('/')).then(() => {
getStore().dispatch({
type: actionTypes.DOC_DELETED,
id,
})
})
})
.catch(err => {
alert('Error occurred deleting doc: ', err.message)
})
}
/**
*
* @param { String|undefined } id - id of doc if update or undefined if new
* @param { String } md - the documents markdown
* @param { String } name - name of document
* @param { String } dir - sub-dir of docsDir for document
* @returns { Promise }
*/
export const updateDoc = (id, md, name, dir) => {
const method = id ? 'PATCH' : 'POST'
const query = id ? `?id=${id}` : ''
const data = {}
if (md) data.md = md
if (name) data.name = name
if (typeof dir === 'string') data.dir = dir
if (name && name.slice(-3) !== '.md') data.name += '.md'
return fetch(addBase(`/docs${query}`), {
headers: {
...getHeaders(),
'content-type': 'application/json',
},
method,
body: JSON.stringify(data),
}).then(async res => {
const { id, ...data } = await res.json()
if (!id) throw new Error(data.message || 'error occurred adding doc')
const docUrl = `/doc?id=${id}`
Router.push(docUrl, addBase(docUrl))
})
}

View File

@@ -0,0 +1,15 @@
import { getStore } from '../store'
/**
* gets headers for xhr request
* @returns { Object } the headers
*/
export default function getHeaders() {
const { jwt } = getStore().getState().user
return !jwt
? false
: {
Authorization: `Bearer ${jwt}`,
}
}

View File

@@ -0,0 +1,92 @@
import { format } from 'url'
import { getStore } from '../store'
import getHeaders from './getHeaders'
import fetch from 'isomorphic-unfetch'
import addBase from '../../util/addBase'
import actionTypes from '../actionTypes'
import config from '../../util/pubConfig'
import {
buildSortBy,
limitDocs,
searchDocs,
sortDocs,
} from '../../util/kbHelpers'
/**
* loads docs
* @param { Object } query - docs query object
* @param { Boolean } forCache - whether this is meant specifically for cache
* @returns { AbortController } instance of abort controller if supported
*/
export default function loadDocs(query, forCache) {
const queryStr = format({ query })
const store = getStore()
const url = addBase(`/docs${queryStr}`, true)
const headers = getHeaders()
if (!headers) return
const { docs, cache } = store.getState()
const fetchIdx = docs.fetchIdx + 1
store.dispatch({ type: actionTypes.DOCS_PENDING, fetchIdx })
let controller
let signal
if (!config.ssr && 'AbortController' in window) {
controller = new AbortController()
signal = controller.signal
}
const req = fetch(url, { headers, signal })
.then(async res => {
let data = await res.json()
if (res.status !== 200) throw new Error(data.message)
if (data.id) data = { results: [data] }
store.dispatch({
type: forCache ? actionTypes.CACHE_DOCS : actionTypes.DOCS_LOADED,
fetchIdx,
data: {
...data,
page: query.page || 1,
},
})
})
.catch(err => {
if (err.name === 'AbortError') return
// try cached docs if offline
if (err.message === 'Failed to fetch') {
// handle fetching from cache
let cacheDocs
if (query.search) cacheDocs = searchDocs(cache, query.search)
cacheDocs = sortDocs(
cache,
buildSortBy(query),
cacheDocs && cacheDocs.map(doc => doc.id)
)
store.dispatch({
type: actionTypes.DOCS_LOADED,
fetchIdx,
data: {
...limitDocs(
cacheDocs,
query,
config.defDocsLimit,
config.maxDocsLimit
),
page: query.page || 1,
},
})
} else {
store.dispatch({
type: actionTypes.DOCS_ERROR,
error: err.message,
fetchIdx,
})
}
})
if (config.ssr) return req
// return controller to abort request
return controller
}

17
src/client/util/logout.js Normal file
View File

@@ -0,0 +1,17 @@
import { getStore } from '../store'
import addBase from '../../util/addBase'
import actionTypes from '../actionTypes'
export default function logout(e) {
e && e.preventDefault()
delete localStorage.jwt
fetch(addBase('/logout'), {
method: 'GET',
credentials: 'include',
}).then(() => {
getStore().dispatch({
type: actionTypes.USER_LOGOUT,
})
})
}

View File

@@ -0,0 +1,37 @@
const basePath = new URL(location).searchParams.get('basePath')
const offlinePath = basePath + 'offline'
if (basePath !== '/') {
const curBase =
basePath.slice(-1) === '/'
? basePath.substr(0, basePath.length - 1)
: basePath
for (let item of self.precacheConfig) {
item[0] = curBase + item[0]
}
}
self.addEventListener('install', event => {
var offlineRequest = new Request(offlinePath)
event.waitUntil(
fetch(offlineRequest).then(response => {
return caches.open('offline').then(cache => {
return cache.put(offlineRequest, response)
})
})
)
})
self.addEventListener('fetch', event => {
const { request } = event
const { method, headers } = request
if (method === 'GET' && headers.get('accept').includes('text/html')) {
event.respondWith(
fetch(request).catch(err => {
return caches.open('offline').then(cache => {
return cache.match(offlinePath)
})
})
)
}
})

View File

@@ -0,0 +1,8 @@
import addBase from '../../util/addBase'
import config from '../../util/pubConfig'
;(function() {
if (!config.dev && !config.ssr && 'serviceWorker' in navigator) {
const basePath = encodeURIComponent(addBase('/'))
navigator.serviceWorker.register(addBase(`sw.js?basePath=${basePath}`))
}
})()

View File

@@ -1,63 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import fetch from 'isomorphic-unfetch'
import mapUser from '../util/mapUser'
import getUrl from '../util/getUrl'
import getJwt from '../util/getJwt'
const getDoc = async (id, req) => {
let found, doc
const jwt = getJwt(req)
if (!jwt) return { found, doc, id }
const docRes = await fetch(getUrl('docs/' + id, Boolean(req)), {
method: 'GET',
headers: { Authorization: jwt },
})
if (docRes.ok) {
doc = await docRes.json()
found = true
}
return { found, doc, id }
}
export default ComposedComponent => {
class DocComp extends Component {
state = {
found: false,
id: null,
doc: {},
}
static async getInitialProps({ query, req }) {
return await getDoc(query.id, req)
}
static getDerivedStateFromProps(nextProps, prevState) {
const { found, id, doc } = nextProps
if (prevState.found !== found && !prevState.didInit) {
return { found, id, doc, didInit: true }
}
return null
}
updateDoc = async id => {
this.setState(await getDoc(id))
}
componentDidMount() {
this.updateDoc(this.props.id)
}
componentDidUpdate(prevProps) {
const { user, found, id } = this.props
if (prevProps.user.email === user.email || found) return
if (!user.email) return
this.updateDoc(id)
}
render() {
return <ComposedComponent {...this.state} />
}
}
return connect(mapUser)(DocComp)
}

View File

@@ -1,65 +0,0 @@
import React, { Component } from 'react'
import cm from 'codemirror'
import { getKey, isCtrlKey } from '../util/keys'
if (typeof window !== 'undefined') {
require('codemirror/mode/markdown/markdown')
}
export default class CodeMirror extends Component {
handleChange = () => {
if (!this.editor) return
const value = this.editor.getValue()
if (value !== this.props.value) {
this.props.onChange && this.props.onChange(value)
if (this.editor.getValue() !== this.props.value) {
if (this.state.isControlled) {
this.editor.setValue(this.props.value)
} else {
this.props.value = value
}
}
}
}
checkSubmit = (cm, e) => {
const key = getKey(e)
if (isCtrlKey(key)) {
this.ctrlKey = true
} else if (key === 13 && this.ctrlKey) {
this.props.onSubmit()
}
}
handleKeyUp = (cm, e) => {
if (isCtrlKey(getKey(e))) this.ctrlKey = false
}
componentDidMount() {
if (typeof window === 'undefined') return
this.editor = cm.fromTextArea(this.textarea, this.props.options)
this.editor.on('change', this.handleChange)
if (typeof this.props.onSubmit === 'function') {
this.editor.on('keydown', this.checkSubmit)
this.editor.on('keyup', this.handleKeyUp)
this.setupSubmitKey = true
}
}
componentWillUnmount() {
if (this.setupSubmitKey) {
this.editor.off('keydown', this.checkSubmit)
this.editor.off('keyup', this.handleKeyUp)
this.setupSubmitKey = false
}
}
componentDidUpdate() {
if (!this.editor || !this.props.value) return
if (this.editor.getValue() !== this.props.value) {
this.editor.setValue(this.props.value)
}
}
render() {
const { value, className, onChange } = this.props
return (
<div {...{ className }}>
<textarea {...{ value, onChange }} ref={el => (this.textarea = el)} />
</div>
)
}
}

View File

@@ -1,26 +0,0 @@
import Link from 'next/link'
import getUrl from '../util/getUrl'
const DocItem = ({ id, name, dir, updated }) => {
name = dir + (dir.length > 0 ? '/' : '') + name
const as = getUrl('k/' + id)
const href = { pathname: '/k', query: { id } }
return (
<tr>
<td>
<Link {...{ href, as }}>
<a>
<p className="noMargin">
{name}
<span className="float-right">
{new Date(updated).toLocaleDateString('en-US')}
</span>
</p>
</a>
</Link>
</td>
</tr>
)
}
export default DocItem

View File

@@ -1,29 +0,0 @@
import { css } from 'glamor'
import theme from '../styles/theme'
const style = {
textAlign: 'center',
padding: '10px 10px 15px',
background: theme.primaryAlt,
'& p': {
marginBottom: 0,
},
}
const Footer = () => (
<footer className={css(style)}>
<p>
Powered by{' '}
<a
href="//github.com/ijjk/mykb"
target="_blank"
rel="noopener noreferrer"
>
MYKB
</a>
</p>
</footer>
)
export default Footer

View File

@@ -1,174 +0,0 @@
import React, { Component } from 'react'
import { css } from 'glamor'
import theme from '../styles/theme'
import { withRouter } from 'next/router'
import { connect } from 'react-redux'
import { doLogout } from '../redux/actions/userAct'
import Link from 'next/link'
import getUrl from '../util/getUrl'
import mapUser from '../util/mapUser'
const style = {
background: theme.primaryAlt,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: 55,
'& .navbar-brand': {
marginLeft: '0.75em',
marginRight: 'auto',
'& h3': {
marginBottom: 0,
},
},
'& .navbar-burger': {
width: 32,
display: 'none',
marginRight: 10,
'&.active div': {
'&:nth-child(1)': {
transformOrigin: 'center',
transform: 'translateY(8px) rotate(45deg)',
},
'&:nth-child(2)': {
opacity: 0,
},
'&:nth-child(3)': {
transformOrigin: 'left -6px',
transform: 'translateY(8px) rotate(-45deg)',
},
},
'& div': {
transition: 'all ease-in-out 150ms',
width: '100%',
height: 2,
margin: '5px 0',
borderRadius: 1,
background: theme.text,
},
},
'& .navbar-items': {
display: 'inline-flex',
flexDirection: 'row',
'& .active .item, .item:hover': {
background: theme.primary,
},
'& .item': {
margin: 0,
cursor: 'pointer',
padding: '15px 20px',
},
},
'@media screen and (max-width: 840px)': {
'& .navbar-burger': {
display: 'inline-block',
},
'& .navbar-items': {
display: 'block',
overflow: 'hidden',
position: 'fixed',
top: 55,
left: 0,
zIndex: 5,
background: theme.primaryAlt,
width: '100%',
transform: 'scaleY(0)',
transformOrigin: 'top',
transition: 'all ease-in-out 125ms',
'&.active': {
transform: 'scaleY(1)',
overflow: 'auto',
},
'& .item': {
width: '100%',
padding: '5px 0',
textAlign: 'center',
},
},
},
}
const NavLink = ({ children, href, active }) => {
const activeClass = active ? ' active' : ''
return (
<Link href={href} as={getUrl(href)}>
<a className={activeClass}>{children}</a>
</Link>
)
}
const navItems = [['/', 'Home'], ['/new', 'New Doc'], ['/settings', 'Settings']]
class Header extends Component {
state = {
open: false,
}
hideNav = () => this.setState({ open: false })
toggleNav = () =>
this.setState({
open: !this.state.open,
})
isActive = url => getUrl(this.props.router.pathname) === getUrl(url)
logout = e => {
e.preventDefault()
this.hideNav()
doLogout()
}
render() {
const expandClass = this.state.open ? ' active' : ''
const { user } = this.props
return (
<nav
className={'navbar ' + css(style)}
role="navigation"
aria-label="main navigation"
>
<div className="navbar-brand">
<NavLink href="/">
<h3 onClick={this.hideNav}>MYKB</h3>
</NavLink>
</div>
{!user.email
? null
: [
<div
className={'navbar-burger ' + expandClass}
onClick={this.toggleNav}
key="burger"
>
<div />
<div />
<div />
</div>,
<div className={'navbar-items' + expandClass} key="items">
{navItems.map(item => (
<NavLink
key={item[0]}
href={item[0]}
active={this.isActive(item[0])}
>
<p className="item" onClick={this.hideNav}>
{item[1]}
</p>
</NavLink>
))}
<a href="/logout" onClick={this.logout}>
<p className="item">Logout</p>
</a>
</div>,
]}
</nav>
)
}
}
export default withRouter(connect(mapUser)(Header))

View File

@@ -1,71 +0,0 @@
import { Component } from 'react'
import Router from 'next/router'
import getUrl from '../util/getUrl'
import { getKey } from '../util/keys'
import { doLogout } from '../redux/actions/userAct'
/* - keyboard shortcuts
g then h -> navigate home
g then n -> navigate to new doc
g then s -> navigate to settings
g then l -> logout
e (when on doc page) -> edit doc
/ (when on home page) -> focus search
ctrl/cmd + enter -> submit new doc (handled in CodeMirror component)
*/
const keyToUrl = {
72: '/',
78: '/new',
83: '/settings',
}
const keyToEl = {
69: { sel: '#edit', func: 'click' },
191: { sel: '.search', func: 'focus' },
}
class KeyShortcuts extends Component {
handleDown = e => {
const tag = e.target.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA') return
const key = getKey(e)
if (this.prevKey === 71) {
// prev key was g
switch (key) {
case 72:
case 78:
case 83: {
const url = keyToUrl[key]
Router.push(url, getUrl(url))
break
}
case 76: {
setTimeout(doLogout, 1)
break
}
default:
break
}
}
switch (key) {
case 69:
case 191: {
const { sel, func } = keyToEl[key]
const el = document.querySelector(sel)
if (el) setTimeout(() => el[func](), 1)
break
}
default:
break
}
this.prevKey = key
}
componentDidMount() {
window.addEventListener('keydown', this.handleDown)
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleDown)
}
render = () => null
}
export default KeyShortcuts

View File

@@ -1,76 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { doLogin } from '../redux/actions/userAct'
import Spinner from './Spinner'
import PaddedRow from './PaddedRow'
import mapUser from '../util/mapUser'
class Login extends Component {
state = {
email: '',
pass: '',
}
updVal = e => {
const el = e.target
const val = el.value
if (el.getAttribute('type') === 'email') {
return this.setState({ email: val })
}
this.setState({ pass: val })
}
submit = e => {
const { pending } = this.props.user
let { email, pass } = this.state
email = email.trim()
pass = pass.trim()
e.preventDefault()
if (pending || email.length === 0 || pass.length == 0) {
return
}
doLogin({ email, password: pass })
}
render() {
const { pending, error } = this.props.user
return (
<div className="container content">
<PaddedRow amount={25} vCenter>
<h4>Please login to continue</h4>
<form noValidate>
<fieldset>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
autoFocus
placeholder="John@deux.com"
onChange={this.updVal}
/>
<label htmlFor="pass">Pass:</label>
<input
type="password"
id="pass"
name="password"
placeholder="Super secret password..."
onChange={this.updVal}
/>
</fieldset>
<button
className={'float-right' + (pending ? ' disabled' : '')}
onClick={this.submit}
>
{pending ? <Spinner /> : 'Submit'}
</button>
{!error ? null : <p>{error}</p>}
</form>
</PaddedRow>
</div>
)
}
}
export default connect(mapUser)(Login)

View File

@@ -1,11 +0,0 @@
import dynamic from 'next/dynamic'
import freezeSSR from '../util/freezeSSR'
const Markdown = dynamic(import('react-markdown'), freezeSSR('.Markdown'))
const link = props => <a {...props} target="_blank" rel="noopener noreferrer" />
const renderers = { link }
const AddRenderers = ({ className, source }) => (
<Markdown {...{ className, source, renderers }} />
)
export default AddRenderers

View File

@@ -1,184 +0,0 @@
import React, { Component } from 'react'
import Router from 'next/router'
import dynamic from 'next/dynamic'
import getUrl from '../util/getUrl'
import getJwt from '../util/getJwt'
import Page from '../components/Page'
import Markdown from '../components/Markdown'
import updStateFromId from '../util/updStateFromId'
import { checkDir, checkName } from '../util/checkDirParts'
import '../styles/monokai'
import '../styles/codemirror'
const CodeMirrorSkel = () => (
<div className="column">
<textarea style={{ height: 'calc(300px - 1.2rem)', margin: 0 }} />
</div>
)
const CodeMirror = dynamic(
typeof window !== 'undefined' && import('../components/CodeMirror'),
{
loading: CodeMirrorSkel,
ssr: false,
}
)
const initState = {
name: '',
dir: '',
md: '## New Document!!',
editMode: false,
error: null,
pending: false,
}
class MngDoc extends Component {
state = initState
updVal = updStateFromId.bind(this)
updMd = md => this.setState({ md })
submit = async () => {
let { name, md, dir, editMode } = this.state
let data = {
name: checkName(name),
dir: checkDir(dir),
md,
}
const doErr = error => this.setState({ pending: false, error })
const dirErr =
'can only contain A-Z, a-z, 0-9, -, or . and not start or end with .'
if (!data.name)
return doErr(
'Document name ' + (data.name === 0 ? 'can not be empty' : dirErr)
)
if (!data.dir && data.dir !== 0) {
return doErr('Directory ' + dirErr)
} else if (data.dir === 0) {
data.dir = ''
}
if (data.md.trim().length === 0) {
return doErr('Content can not be empty')
}
let url = getUrl('docs'),
method = 'POST',
headers = {
Authorization: getJwt(),
'Content-Type': 'application/json',
}
if (editMode) {
let numRemoved = 0
const dataKeys = Object.keys(data)
dataKeys.forEach(k => {
if (data[k] === this.props.doc[k]) {
delete data[k]
numRemoved++
}
})
if (dataKeys.length === numRemoved) return
url = getUrl('docs/' + this.props.doc.id)
method = 'PATCH'
}
this.setState({ error: null, pending: true })
const res = await fetch(url, {
headers,
method,
body: JSON.stringify(data),
}).catch(doErr)
try {
data = await res.json()
} catch (err) {
data = { message: 'An error occurred submitting doc' }
}
if (res.ok) {
const { id } = data
return Router.push(
{
pathname: '/k',
query: { id },
},
getUrl(`k/${id}`)
)
}
doErr(data.message)
}
static getDerivedStateFromProps(nextProps, prevState) {
const { doc } = nextProps
if (doc && !prevState.didInit) {
const { name, dir, md } = doc
return { name, md, dir, editMode: true, didInit: true }
} else if (!prevState.didInit && prevState.id) {
return { ...initState, didInit: true }
} else if (!prevState.didInit) {
return { didInit: true }
}
return null
}
render() {
const { md, dir, name, error, pending } = this.state
const rowStyle = { paddingTop: 10 }
return (
<Page>
<div className="row fill" style={rowStyle}>
<div className="column column-50">
<Markdown className="fill Markdown" source={md} />
</div>
<div className="column column-50">
<div className="row">
<div className="column column-60">
<input
type="text"
maxLength={250}
placeholder="New document name"
id="name"
value={name}
onChange={this.updVal}
/>
</div>
<div className="column">
<input
type="text"
maxLength={1024}
placeholder="Subdirectory (optional)"
id="dir"
value={dir}
onChange={this.updVal}
/>
</div>
</div>
<div className="row">
<CodeMirror
value={md}
className="column WrapCodeMirror"
onChange={this.updMd}
onSubmit={this.submit}
options={{
theme: 'monokai',
mode: 'markdown',
lineWrapping: true,
}}
/>
</div>
<div className="row" style={{ marginTop: 5 }}>
<div className="column">
<span>{error}</span>
<button
className="float-right"
style={{ marginTop: 5 }}
onClick={pending ? null : this.submit}
>
Submit
</button>
</div>
</div>
</div>
</div>
</Page>
)
}
}
export default MngDoc

View File

@@ -1,16 +0,0 @@
const PaddedRow = ({ children, amount, vCenter }) => {
amount = amount || 20
const PadItem = () => <div className={'column column-' + amount + ' nomob'} />
let rowProps = { className: 'row' }
if (vCenter) rowProps = { className: 'row v-center' }
else rowProps = { ...rowProps, style: { paddingTop: amount } }
return (
<div {...rowProps}>
<PadItem />
<div className="column">{children}</div>
<PadItem />
</div>
)
}
export default PaddedRow

View File

@@ -1,24 +0,0 @@
import { connect } from 'react-redux'
import Header from './Header'
import KeyShortcuts from './KeyShortcuts'
import Footer from './Footer'
import Login from './Login'
import Setup from './Setup'
import mapUser from '../util/mapUser'
const Page = ({ user, children }) => {
return (
<div>
<Header />
<KeyShortcuts />
{(() => {
if (user.email) {
return <div className="container content">{children}</div>
}
return user.setup ? <Setup /> : <Login {...{ user }} />
})()}
<Footer />
</div>
)
}
export default connect(mapUser)(Page)

View File

@@ -1,105 +0,0 @@
import React, { Component } from 'react'
import { doLogin } from '../redux/actions/userAct'
import PaddedRow from './PaddedRow'
import Spinner from './Spinner'
import getUrl from '../util/getUrl'
export default class Setup extends Component {
state = {
email: '',
password: '',
confirmPass: '',
pending: false,
error: null,
}
updVal = e => {
const el = e.target
let key = 'email'
if (el.id === 'pass') key = 'password'
else if (el.id === 'pass2') key = 'confirmPass'
const obj = {}
obj[key] = el.value
this.setState(obj)
}
submit = e => {
e.preventDefault()
let { email, password, confirmPass, pending } = this.state
if (pending) return
email = email.trim()
password = password.trim()
confirmPass = confirmPass.trim()
const hasEmpty = [email, password, confirmPass].some(
val => val.length === 0
)
if (hasEmpty) return
if (password.toLowerCase() !== confirmPass.toLowerCase()) {
return this.setState({ error: "Passwords don't match" })
}
this.setState({ pending: true, error: null })
const defaultErr = 'Could not create account'
fetch(getUrl('users'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, admin: true }),
})
.then(res => {
if (res.ok) {
return doLogin({ email, password }, null, true)
}
res.json().then(({ message }) => {
const error = message || defaultErr
this.setState({ pending: false, error })
})
})
.catch(err => {
const error = err.message || defaultErr
this.setState({ pending: false, error })
})
}
render() {
const { pending, error } = this.state
return (
<div className="container content">
<PaddedRow amount={25} vCenter>
<div className="column">
<h3>Setup account</h3>
<form noValidate>
<fieldset>
<label htmlFor="email">Email:</label>
<input
type="email"
autoFocus
id="email"
placeholder={"Your email (does't have to be actual email)"}
onChange={this.updVal}
/>
<label htmlFor="pass">Password:</label>
<input
type="password"
id="pass"
maxLength={512}
placeholder="A super secret password"
onChange={this.updVal}
/>
<label htmlFor="pass2">Confirm Password:</label>
<input
type="password"
id="pass2"
maxLength={512}
placeholder="Confirm your super secret password"
onChange={this.updVal}
/>
<button className="float-right" onClick={this.submit}>
{pending ? <Spinner /> : 'Submit'}
</button>
{!error ? null : <p className="danger">{error}</p>}
</fieldset>
</form>
</div>
</PaddedRow>
</div>
)
}
}

View File

@@ -1,2 +0,0 @@
const Spinner = props => <div className="spinner" {...props} />
export default Spinner

View File

@@ -1,23 +0,0 @@
// A hook that logs service method before, after and error
// See https://github.com/winstonjs/winston for documentation
// about the logger.
const logger = require('winston')
// To see more detailed messages, uncomment the following line
// logger.level = 'debug';
module.exports = function() {
return context => {
// This debugs the service call and a stringified version of the hook context
// You can customize the mssage (and logger) to your needs
// logger.debug(
// `${context.type} app.service('${context.path}').${context.method}()`
// )
// if (typeof context.toJSON === 'function') {
// logger.debug('Hook Context', JSON.stringify(context, null, ' '))
// }
// if (context.error) {
// logger.error(context.error)
// }
}
}

View File

@@ -1,11 +1,60 @@
const logger = require('winston') const http = require('http')
const app = require('./app') const path = require('path')
const port = app.get('port') const Next = require('next')
const chokidar = require('chokidar')
const isDev = process.env.NODE_ENV !== 'production'
const next = Next({ dev: isDev, quiet: true })
app.run(port).then(() => { let server = null
logger.info('MYKB listening at http://%s:%d', app.get('host'), port) let creatingServer = false
})
process.on('unhandledRejection', (reason, p) => // prepare next
logger.error('Unhandled Rejection at: Promise ', p, reason) next.prepare()
) global.next = next
async function createServer() {
if (creatingServer) return
creatingServer = true
const { port } = await require('./server/util/config')
if (server) {
console.log('Restarting server...')
await new Promise(resolve => {
server.destroy(() => resolve())
})
}
try {
server = http.createServer(require('./server/server'))
isDev && require('server-destroy')(server)
server.listen(port)
server.once('listening', () => {
creatingServer = false
console.log(`Listening at http://127.0.0.1:${port}`)
})
} catch (err) {
console.error(err)
creatingServer = false
console.log('waiting for change to restart...')
}
}
if (isDev) {
// watch for server changes and hot reload server
// without having to reload next.js
const configPath = path.resolve('./config')
const utilPath = path.resolve('./src/util')
const serverPath = path.resolve('./src/server')
const watcher = chokidar.watch([serverPath, configPath, utilPath], {})
watcher.on('change', path => {
Object.keys(require.cache).forEach(key => {
if (key.indexOf(serverPath) > -1 || key.indexOf(utilPath) > -1) {
delete require.cache[key]
delete module.constructor._pathCache[key]
}
})
createServer()
})
}
createServer()

View File

@@ -1,11 +0,0 @@
// eslint-disable-next-line no-unused-vars
module.exports = function(app) {
// Add your custom middleware here. Remember, that
// in Express the order matters
// add req.ip to feathers
app.use((req, res, next) => {
req.feathers.ip = req.ip
next()
})
}

View File

@@ -1,14 +0,0 @@
const NeDB = require('nedb')
const path = require('path')
module.exports = function(app) {
const dbPath = app.get('nedb')
const Model = new NeDB({
filename: path.join(dbPath, 'users.db'),
autoload: true,
})
Model.ensureIndex({ fieldName: 'email', unique: true })
return Model
}

View File

@@ -1,82 +0,0 @@
import fetch from 'isomorphic-unfetch'
import store from '../store'
import getUrl from '../../util/getUrl'
// define action types
export const SET_USER = 'SET_USER'
export const LOGIN_PENDING = 'LOGIN_PENDING'
export const LOGIN_FAILED = 'LOGIN_FAILED'
export const LOGOUT = 'LOGOUT'
export const setUser = user => {
store.dispatch({
type: SET_USER,
data: user,
})
} // setUser
export const doLogout = () => {
if (typeof window !== 'undefined') {
window.localStorage.removeItem('jwt')
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;'
}
store.dispatch({ type: LOGOUT })
} // doLogout
export async function doLogin(creds, jwt, noPend) {
!noPend && store.dispatch({ type: LOGIN_PENDING })
const authReqOpts = { method: 'POST', credentials: 'include' }
const authReqHead = {
headers: jwt
? { Authorization: jwt }
: {
'Content-Type': 'application/json',
},
}
const authReqBody = jwt
? null
: {
body: JSON.stringify({ ...creds, strategy: 'local' }),
}
const authReq = new Request(getUrl('auth'), {
...authReqOpts,
...authReqHead,
...authReqBody,
})
const authRes = await fetch(authReq).catch(err => {
store.dispatch({ type: LOGIN_FAILED, data: err.message })
})
if (!authRes.ok) {
let error
try {
error = await authRes.json()
error = error.message
} catch (err) {
error =
authRes.status === 429
? 'Max login attempts reached'
: 'An error occurred during login'
}
return store.dispatch({
type: LOGIN_FAILED,
data: error,
})
}
const { accessToken } = await authRes.json()
const payload = accessToken.split('.')[1]
const { userId } = JSON.parse(atob(payload))
const userReq = new Request(getUrl(`/users/${userId}`), {
headers: {
Authorization: accessToken,
},
})
const userRes = await fetch(userReq)
if (!userRes.ok) {
return store.dispatch({
type: LOGIN_FAILED,
data: 'failed to get user',
})
}
window.localStorage.setItem('jwt', accessToken)
setUser(await userRes.json())
} // doLogin

View File

@@ -1,46 +0,0 @@
import {
SET_USER,
LOGIN_PENDING,
LOGIN_FAILED,
LOGOUT,
} from '../actions/userAct'
const initState = {
setup: false,
_id: null,
email: null,
admin: null,
pending: false,
error: null,
}
function user(state = initState, action) {
switch (action.type) {
case SET_USER: {
return {
...initState,
...action.data,
}
}
case LOGIN_PENDING: {
return {
...initState,
pending: true,
}
}
case LOGIN_FAILED: {
return {
...state,
pending: false,
error: action.data,
}
}
case LOGOUT: {
return initState
}
default:
return state
}
}
export default user

View File

@@ -1,19 +0,0 @@
import { applyMiddleware, combineReducers, createStore } from 'redux'
import user from './reducers/userRed'
let middleware
if (process.env.NODE_ENV !== 'production') {
const logger = require('redux-logger').default
if (typeof window !== 'undefined') {
middleware = applyMiddleware(logger)
}
}
const reducers = combineReducers({
user,
})
export default (middleware
? createStore(reducers, middleware)
: createStore(reducers))

90
src/server/middleware.js Normal file
View File

@@ -0,0 +1,90 @@
const path = require('path')
const DB = require('./util/db')
const bcrypt = require('bcrypt')
const helmet = require('helmet')
const express = require('express')
const passport = require('passport')
const bodyParser = require('body-parser')
const compression = require('compression')
const cookieParser = require('cookie-parser')
const { Strategy } = require('passport-local')
const jwtExtract = require('./util/jwtExtract')
const { Strategy: JwtStrategy } = require('passport-jwt')
const publicDir = path.join(__dirname, '../../public')
const usersDb = path.join(__dirname, '../../config/users.json')
const users = new DB(usersDb)
passport.use(
new Strategy(function(username, password, cb) {
let user = null
const found = Object.keys(users.data).some(id => {
const curUser = users.data[id]
if (curUser.username === username) {
user = curUser
return true
}
})
if (!found) return cb(null, false)
bcrypt.compare(password, user.password).then(match => {
cb(null, match && user)
})
})
)
passport.serializeUser(function(user, cb) {
cb(null, user.id)
})
passport.deserializeUser(function(id, cb) {
const { password, ...user } = users.data[id] || {}
cb(null, user.id && user)
})
module.exports = function middleware(app) {
const jwtOpts = {
...app.config.jwt,
jwtFromRequest: jwtExtract,
secretOrKey: app.config.secret,
}
passport.use(
new JwtStrategy(jwtOpts, function(jwtPayload, cb) {
const id = jwtPayload.user.id
const { password, ...user } = users.data[id]
return cb(null, user)
})
)
// serve public path
app.use('/', express.static(publicDir))
// add gzip support
app.use(compression())
// add helpful headers
app.use(helmet({ hidePoweredBy: { setTo: 'hamsters' } }))
// set up passport.js
app.use(cookieParser())
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(passport.initialize())
app.use((req, res, next) => {
passport.authenticate('jwt', function(err, user, info) {
if (user) req.user = user
next()
})(req, res, next)
})
app.use((req, res, next) => {
if (!Object.keys(users.data).length) {
global.publicConfig.doSetup = true
}
next()
})
app.users = users
app.passport = passport
}

33
src/server/routes/auth.js Normal file
View File

@@ -0,0 +1,33 @@
const rateLimit = require('express-rate-limit')
const handleLogin = require('../util/handleLogin')
const { userError } = require('../util/responses')
/**
* sets up auth endpoint
*/
module.exports = function auth(app) {
const { passport } = app
if (!app.config.dev) {
const authLimiter = rateLimit({
windowMs: 2 * 60 * 1000, // 2 minutes
max: 10, // limit each IP to 10 requests per windowMs
})
app.use('/auth', authLimiter)
}
app.post('/auth', (req, res) => {
// allow checking JWT with Authorization header
if (req.get('authorization')) {
return res
.status(req.user ? 200 : 400)
.json({ status: req.user ? 'ok' : 'error' })
}
passport.authenticate('local', {}, (err, user, info) => {
if (err || !user) {
return userError(res, 'Invalid login')
}
handleLogin(req, res, user, app)
})(req, res)
})
}

168
src/server/routes/docs.js Normal file
View File

@@ -0,0 +1,168 @@
const path = require('path')
const fs = require('fs-extra')
const KB = require('../util/kb')
const Git = require('simple-git/promise')
const isOkDoc = require('../util/isOkDoc')
const tryRmdir = require('../util/tryRmdir')
const requireUser = require('../util/requireUser')
const { aOk, notFound, serverError, userError } = require('../util/responses')
const {
sortDocs,
searchDocs,
limitDocs,
buildSortBy,
} = require('../../util/kbHelpers')
/**
* sets up docs endpoint
*/
module.exports = function docs(app) {
let git
const {
useGit,
docsDir,
cacheSize,
maxDocSize,
maxDocsLimit,
defDocsLimit,
} = app.config
const kb = new KB({
cacheSize,
maxDocSize,
kbPath: docsDir,
})
if (useGit) {
git = Git(docsDir)
kb.loaded.then(() => {
const numDocs = Object.keys(kb.docs).length
// check if git needs to be initialized in docs dir
fs.stat(path.join(docsDir, '.git'), err => {
if (err && err.code === 'ENOENT') {
git.init().then(() => {
git.addConfig('user.name', 'mykb')
git.addConfig('user.email', 'mykb@localhost')
if (numDocs === 0) return
git.add('./*').then(() => git.commit('initial commit'))
})
}
})
})
}
// make sure user is logged in
app.use('/docs*', requireUser)
// handle getting docs
app.get('/docs', (req, res) => {
let docs
const { query } = req
if (query.id) {
const doc = kb.docs[query.id]
if (!doc) return notFound(res)
return aOk(res, doc)
}
if (query.search) {
docs = searchDocs(kb.docs, query.search)
}
const sortBy = buildSortBy(query)
docs = sortDocs(kb.docs, sortBy, docs && docs.map(doc => doc.id))
const { total, hasMore, results } = limitDocs(
docs,
query,
defDocsLimit,
maxDocsLimit
)
aOk(res, { total, results, hasMore })
})
// handle new doc
app.post('/docs', async (req, res) => {
const { name, dir, md } = req.body
if (!isOkDoc({ name, dir, md }, kb, res, true)) return
try {
const docPath = path.join(docsDir, dir || '', name)
await fs.outputFile(docPath, md)
useGit && (await git.add(docPath))
useGit && (await git.commit(`added doc ${docPath.split(docsDir)[1]}`))
} catch (err) {
return serverError(res, err)
}
const added = new Date()
const doc = kb.setDoc(path.join(dir || '', name), added, md, added)
aOk(res, doc)
})
// handle update
app.patch('/docs', async (req, res) => {
const { query, body } = req
const doc = kb.docs[query.id]
const { name, dir, md } = body
if (!doc) return notFound(res)
if (!isOkDoc({ name: name || doc.name, dir, md }, kb, res)) return
let newDir = typeof dir === 'string' ? dir : doc.dir
const oldDir = path.join(docsDir, doc.dir)
const oldPath = path.join(oldDir, doc.name)
const docPath = path.join(docsDir, newDir, name || doc.name)
const oldRelPath = oldPath.split(docsDir + '/')[1]
const curRelPath = docPath.split(docsDir + '/')[1]
const isNewPath = oldPath !== docPath
if (isNewPath && kb.docs[curRelPath]) {
return userError(res, 'item already exists at new path')
}
try {
if (md) {
await fs.outputFile(docPath, md)
isNewPath && (await fs.remove(oldPath))
} else if (isNewPath) {
await fs.move(oldPath, docPath)
}
await tryRmdir(docsDir, oldRelPath)
if (useGit && isNewPath) {
await git.rm(oldPath)
await git.add(docPath)
await git.commit(`renamed doc ${oldRelPath} to ${curRelPath}`)
} else if (useGit) {
await git.add(docPath)
await git.commit(`updated doc ${curRelPath}`)
}
} catch (err) {
return serverError(res, err)
}
const updated = new Date()
const updatedDoc = kb.setDoc(curRelPath, doc.created, md || doc.md, updated)
aOk(res, updatedDoc)
})
// handle delete doc
app.delete('/docs', async (req, res) => {
const { query } = req
const doc = { ...kb.docs[query.id] }
if (!doc) {
return notFound(res)
}
const toRemove = path.join(docsDir, doc.id)
try {
await fs.remove(toRemove)
// try removing directory to clean up if was only file in dir
if (doc.dir) {
tryRmdir(docsDir, doc.dir)
}
useGit && (await git.rm(doc.id))
useGit && (await git.commit(`removed doc ${doc.id}`))
} catch (err) {
return serverError(res, err)
}
delete kb.docs[query.id]
aOk(res)
})
}

View File

@@ -0,0 +1,11 @@
const addBase = require('../../util/addBase')
/**
* sets up logout endpoint
*/
module.exports = function logout(app) {
app.get('/logout', (req, res) => {
res.clearCookie('jwt')
res.redirect(addBase('/'))
})
}

View File

@@ -0,0 +1,38 @@
const bcrypt = require('bcrypt')
const uuid = require('uuid/v4')
const handleLogin = require('../util/handleLogin')
/**
* sets up register endpoint
*/
module.exports = function register(app) {
const { users } = app
app.post('/register', (req, res) => {
if (!global.publicConfig.doSetup) {
return res.status(401).json({ message: 'already set up' })
}
const { username, password } = req.body
const taken = Object.keys(users.data).some(id => {
const user = users.data[id]
return user.username === username
})
if (taken) {
return res.send('username taken')
}
bcrypt.hash(password, 10).then(hash => {
const id = uuid()
const user = {
id,
username,
password: hash,
}
users.setData({
...users.data,
[id]: user,
})
delete global.publicConfig.doSetup
handleLogin(req, res, user, app)
})
})
}

44
src/server/routes/user.js Normal file
View File

@@ -0,0 +1,44 @@
const bcrypt = require('bcrypt')
const rateLimit = require('express-rate-limit')
const { aOk, userError, serverError } = require('../util/responses')
/**
* allows updating user
*/
module.exports = function auth(app) {
const { passport, users } = app
if (!app.config.dev) {
const authLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 10, // limit each IP to 10 requests per windowMs
})
app.use('/user', authLimiter)
}
app.patch('/user', (req, res) => {
passport.authenticate('local', {}, (err, user, info) => {
if (err || !user) {
return userError(res, 'Invalid login')
}
const { id } = user
let { newPassword } = req.body
if (!newPassword) {
return userError(res, 'newPassword is required')
}
newPassword = bcrypt.hash(newPassword, 10).then(hash => {
users
.setData({
...users.data,
[id]: {
...users.data[id],
password: hash,
},
})
.then(() => aOk(res))
.catch(err => serverError(res, err))
})
})(req, res)
})
}

60
src/server/server.js Normal file
View File

@@ -0,0 +1,60 @@
const path = require('path')
const express = require('express')
const config = require('./util/config')
const middleware = require('./middleware')
const trustIPs = require('./util/trustIPs')
const app = express()
const router = express.Router()
const next = global.next
async function main() {
router.config = await config
const {
dev,
ssr,
port,
date,
basePath,
searchDelay,
maxDocsLimit,
defDocsLimit,
trustCloudflare,
} = router.config
// resolve paths to absolute paths
router.config.docsDir = path.resolve(router.config.docsDir)
// config made public to the client
global.publicConfig = {
dev,
ssr,
port,
date,
basePath,
searchDelay,
maxDocsLimit,
defDocsLimit,
}
// set up proxy trusting
trustIPs(app, trustCloudflare)
// set next to use basePath
next.setAssetPrefix(basePath)
// apply middleware
middleware(router)
const routes = ['auth', 'logout', 'register', 'docs', 'user']
// set up routes
routes.forEach(route => require('./routes/' + route)(router))
// set up next handler (must come last)
router.use(next.getRequestHandler())
// prefix all routes with baseRoute
app.use(basePath, router)
}
main()
module.exports = app

31
src/server/util/config.js Normal file
View File

@@ -0,0 +1,31 @@
const DB = require('./db')
const path = require('path')
const isDev = process.env.NODE_ENV !== 'production'
const configPath = file => path.join(__dirname, '../../../config', file)
const defaultPath = configPath('default.json')
const productionPath = configPath('production.json')
const defaultConfig = new DB(defaultPath)
const productionConfig = isDev
? { loading: Promise.resolve(), data: {} }
: new DB(productionPath)
/**
* @returns {Promise} - resolves with config when loaded
*/
function config() {
return defaultConfig.loading
.then(() => productionConfig.loading)
.then(() => {
return {
...defaultConfig.data,
...productionConfig.data,
ssr: true,
dev: isDev,
setData: (isDev ? defaultConfig : productionConfig).setData,
}
})
}
module.exports = config()

24
src/server/util/db.js Normal file
View File

@@ -0,0 +1,24 @@
const fs = require('fs-extra')
// read and write to a json file
module.exports = function DB(file) {
this.data = {}
this.setData = function(data) {
this.data = data
return fs.writeFile(file, JSON.stringify(data, null, 2))
}
this.getData = function() {
return this.data
}
this.loading = fs
.readFile(file)
.then(buf => {
this.data = JSON.parse(buf.toString())
})
.catch(err => {
console.error(err)
})
}

View File

@@ -0,0 +1,29 @@
const jwt = require('jsonwebtoken')
const addBase = require('../../util/addBase')
module.exports = function(req, res, user, app) {
req.login(user, {}, err => {
if (err) {
return res.send(err)
}
const curUser = Object.keys(user).reduce((obj, key) => {
if (key !== 'password') obj[key] = user[key]
return obj
}, {})
const token = jwt.sign({ user: curUser }, app.config.secret, app.config.jwt)
res.cookie('jwt', token, {
expires: new Date(Date.now() + 3600 * 24 * 7 * 1000),
httpOnly: true,
})
// redirect if using form submission instead of XHR
if (req.body.form) {
return res.redirect(addBase('/'))
}
res.json({ accessToken: token })
})
}

View File

@@ -0,0 +1,34 @@
const path = require('path')
const { userError } = require('./responses')
const isOkDirPart = require('../../util/isOkDirPart')
/**
* checks if doc request is ok
* @param { Object } doc - doc object with name, dir, md
* @param { Object } kb - the current kb instance
* @param { Object } res - the express.Response object
* @param { boolean } requireAll - whether to require all fields on doc
*/
module.exports = function isOkDoc({ name, dir, md }, kb, res, requireAll) {
let docPath = name
if (!md || typeof md !== 'string' || md.length === 0) {
if (requireAll) return userError(res, 'md can not be empty')
}
if (name && name.slice(-3) !== '.md') {
return userError(res, 'doc name must end in .md')
} else if (name && !isOkDirPart(name)) {
return userError(res, 'name contains an invalid character')
}
if (dir && typeof dir === 'string') {
if (dir.split('/').some(dirPart => !isOkDirPart(dirPart))) {
return userError(res, 'dir contains an invalid character')
}
docPath = path.join(dir, name)
}
if (requireAll && kb.docs[docPath]) {
return userError(res, 'item already exists')
}
return true
}

View File

@@ -0,0 +1,19 @@
const { ExtractJwt } = require('passport-jwt')
const bearerExtract = ExtractJwt.fromAuthHeaderAsBearerToken()
const ssrRoutes = {
'/': 1,
'/doc': 1,
'/new': 1,
'/edit': 1,
'/settings': 1,
}
module.exports = function jwtExtract(req) {
let token = null
if (ssrRoutes[req.path]) {
token = req.cookies.jwt
} else {
token = bearerExtract(req)
}
return token
}

94
src/server/util/kb.js Normal file
View File

@@ -0,0 +1,94 @@
const path = require('path')
const fs = require('fs-extra')
const chokidar = require('chokidar')
/**
* loads markdown files from docs directory
* and watches for changes
* @param { Object } options - the options object
*/
module.exports = function KB({
kbPath = '',
cacheSize = 10 << 20, // 10 MB
maxDocSize = 100 << 10, // 100 KB
}) {
this.docs = {}
this.cachedSize = 0
/**
* update doc entry (does not update doc file)
* @param { String } relPath - relative path of doc to docsDir
* @param { Date } created - date string of when doc was created
* @param { String } md - the markdown string
* @param { Date } updated - date string of when doc was last updated
* @returns { Object } - the new doc entry
*/
this.setDoc = (relPath, created, md, updated) => {
const id = relPath
let dir = relPath.split('/')
const name = dir.pop()
dir = dir.join('/')
const doc = {
name,
dir,
id,
md,
created,
updated,
}
this.docs[id] = doc
return doc
}
/**
* @param { String } path - the absolute path to doc
* @returns { String } - relative path from docs directory
*/
this.getRelPath = path => {
path = path.split(kbPath).pop()
if (path.substr(0, 1) === '/') path = path.substr(1)
return path
}
/**
* handle doc add or change
* @param { String } path - absolute path to doc
* @param { Object|undefined } stats - the fs.stats object for the doc
*/
this.handleDoc = async (path, stats) => {
const relPath = this.getRelPath(path)
if (!stats) stats = await fs.stat(path)
else {
// stats were set so it's a change event
const changedDoc = this.docs[relPath]
if (changedDoc && changedDoc.md) this.cached -= changedDoc.md.length
}
const { birthtime, size, mtime } = stats
let md = null
if (size < maxDocSize && this.cachedSize + size < cacheSize) {
md = await fs.readFile(path)
md = md.toString()
this.cached += md.length
}
this.setDoc(relPath, birthtime, md, mtime)
}
// set up watching of docs dir and populate initial data
this.watcher = chokidar.watch(path.join(kbPath, '/**/*.md'))
this.loaded = new Promise(resolve => {
this.watcher.on('ready', resolve)
})
this.watcher.on('add', this.handleDoc)
this.watcher.on('change', this.handleDoc)
this.watcher.on('unlink', path => {
// doc removed
const id = this.getRelPath(path)
if (this.docs[id] && this.docs[id].md) {
this.cached -= this.docs[id].md.length
}
delete this.docs[id]
})
}

View File

@@ -0,0 +1,17 @@
/**
* returns forbidden status when user not present
* @param {Object} - express.Request
* @param {Object} - express.Response
* @returns {boolean} - whether if had user or not
*/
module.exports = function requireUser(req, res, next) {
if (!req.user) {
res.status(403).json({
status: 'error',
message: 'You do not have permission to access this',
})
return false
}
if (next) next()
return true
}

View File

@@ -0,0 +1,30 @@
const isDev = process.env.NODE_ENV !== 'production'
// standard JSON responses
module.exports = {
aOk: (res, data) => {
res.status(200).json({ status: 'ok', ...data })
},
notFound: res => {
res.status(404).json({
status: 'error',
message: 'item not found',
})
},
serverError: (res, err) => {
isDev && console.log(new Error(err).stack)
res.status(500).json({
status: 'error',
message: (isDev && err && err.message) || 'server encountered error',
})
},
userError: (res, message) => {
res.status(400).json({
status: 'error',
message,
})
},
}

View File

@@ -1,13 +1,12 @@
/* eslint-disable no-console */
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const fetch = require('isomorphic-unfetch') const fetch = require('isomorphic-unfetch')
const ips = require('../config/trustIPs.json') const ips = require('../../../config/trustIPs.json')
ips.push('loopback') ips.push('loopback')
const cfv4 = 'https://www.cloudflare.com/ips-v4' const cfv4 = 'https://www.cloudflare.com/ips-v4'
const cfv6 = 'https://www.cloudflare.com/ips-v6' const cfv6 = 'https://www.cloudflare.com/ips-v6'
const cfConf = path.join(__dirname, '../config/cfIPs.json') const cfConf = path.resolve('./config/cfIPs.json')
const refreshInterval = 24 * 60 * 60 * 1000 const refreshInterval = 24 * 60 * 60 * 1000
const getIps = str => { const getIps = str => {
@@ -32,8 +31,8 @@ const getCfIps = async app => {
app.set('trust proxy', [...ips, ...cfIps]) app.set('trust proxy', [...ips, ...cfIps])
} }
module.exports = app => { module.exports = (app, cloudflare = false) => {
if (!app.get('trustCloudflare')) { if (!cloudflare) {
return app.set('trust proxy', ips) return app.set('trust proxy', ips)
} }
fs.readFile(cfConf, async (err, buff) => { fs.readFile(cfConf, async (err, buff) => {

Some files were not shown because too many files have changed in this diff Show More