浏览代码

Merge branch 'dev' into master

Nicolas Giard 7 年之前
父节点
当前提交
05c32fb1bd
共有 100 个文件被更改,包括 2963 次插入2519 次删除
  1. 20 0
      .babelrc
  2. 2 15
      .eslintrc.json
  3. 2 1
      .npmrc
  4. 2 1
      .yarnrc
  5. 0 5
      CHANGELOG.md
  6. 二进制
      assets/images/bg.jpg
  7. 二进制
      assets/images/bg_1.jpg
  8. 二进制
      assets/images/bg_2.jpg
  9. 二进制
      assets/images/bg_3.jpg
  10. 8 0
      assets/svg/auth-icon-azure.svg
  11. 3 0
      assets/svg/auth-icon-facebook.svg
  12. 0 0
      assets/svg/auth-icon-github.svg
  13. 1 0
      assets/svg/auth-icon-google.svg
  14. 10 0
      assets/svg/auth-icon-ldap.svg
  15. 8 0
      assets/svg/auth-icon-local.svg
  16. 1 0
      assets/svg/auth-icon-microsoft.svg
  17. 1 0
      assets/svg/auth-icon-slack.svg
  18. 7 0
      assets/svg/login-bg-dark.svg
  19. 7 0
      assets/svg/login-bg.svg
  20. 0 4
      client/configure.js
  21. 2 15
      client/index.js
  22. 51 47
      client/js/app.js
  23. 153 0
      client/js/app.old.js
  24. 239 0
      client/js/components/config-manager.component.js
  25. 3 3
      client/js/components/editor-video.vue
  26. 70 0
      client/js/components/login.vue
  27. 13 14
      client/js/components/page-loader.vue
  28. 22 0
      client/js/constants/graphql.js
  29. 5 0
      client/js/constants/index.js
  30. 1 0
      client/js/helpers/index.js
  31. 0 7
      client/js/login.js
  32. 52 0
      client/js/modules/localization.js
  33. 3 1
      client/scss/app.scss
  34. 6 1
      client/scss/base/base.scss
  35. 18 4
      client/scss/components/button.scss
  36. 61 0
      client/scss/components/config-manager.scss
  37. 0 13
      client/scss/login.scss
  38. 263 305
      client/scss/pages/_login.scss
  39. 22 138
      config.sample.yml
  40. 0 14
      npm/README.md
  41. 0 25
      npm/install.js
  42. 0 32
      npm/package.json
  43. 132 125
      package.json
  44. 0 222
      server/agent.js
  45. 45 57
      server/app/data.yml
  46. 37 0
      server/authentication/azure.js
  47. 32 0
      server/authentication/facebook.js
  48. 32 0
      server/authentication/github.js
  49. 31 0
      server/authentication/google.js
  50. 46 0
      server/authentication/ldap.js
  51. 38 0
      server/authentication/local.js
  52. 31 0
      server/authentication/microsoft.js
  53. 31 0
      server/authentication/slack.js
  54. 42 90
      server/configure.js
  55. 32 32
      server/controllers/admin.js
  56. 28 27
      server/controllers/auth.js
  57. 16 13
      server/controllers/uploads.js
  58. 19 252
      server/index.js
  59. 0 90
      server/init.js
  60. 0 261
      server/libs/auth.js
  61. 0 68
      server/libs/config.js
  62. 0 63
      server/libs/db.js
  63. 0 77
      server/libs/logger.js
  64. 0 81
      server/libs/search-index/index.js
  65. 0 36
      server/libs/search-index/siUtil.js
  66. 1 1
      server/locales/ja/auth.json
  67. 186 0
      server/master.js
  68. 1 1
      server/middlewares/flash.js
  69. 1 3
      server/middlewares/security.js
  70. 16 0
      server/models/_relations.js
  71. 0 20
      server/models/bruteforce.js
  72. 16 0
      server/models/comment.js
  73. 64 0
      server/models/document.js
  74. 0 42
      server/models/entry.js
  75. 42 0
      server/models/file.js
  76. 22 0
      server/models/folder.js
  77. 16 0
      server/models/group.js
  78. 36 0
      server/models/right.js
  79. 22 18
      server/models/setting.js
  80. 22 0
      server/models/tag.js
  81. 0 46
      server/models/upl-file.js
  82. 0 21
      server/models/upl-folder.js
  83. 104 93
      server/models/user.js
  84. 106 0
      server/modules/auth.js
  85. 85 0
      server/modules/config.js
  86. 137 0
      server/modules/db.js
  87. 27 35
      server/modules/disk.js
  88. 6 6
      server/modules/documents.js
  89. 32 59
      server/modules/git.js
  90. 43 0
      server/modules/graphql.js
  91. 91 0
      server/modules/kernel.js
  92. 52 0
      server/modules/localization.js
  93. 80 0
      server/modules/logger.js
  94. 9 7
      server/modules/markdown.js
  95. 37 0
      server/modules/queue.js
  96. 33 0
      server/modules/redis.js
  97. 3 3
      server/modules/rights.js
  98. 13 13
      server/modules/search.js
  99. 0 0
      server/modules/system.js
  100. 12 12
      server/modules/uploads-agent.js

+ 20 - 0
.babelrc

@@ -0,0 +1,20 @@
+{
+  "comments": false,
+  "presets": [
+    ["env", {
+      "targets": {
+        "browsers": [
+          "last 6 Chrome major versions",
+          "last 6 Firefox major versions",
+          "last 4 Safari major versions",
+          "last 4 Edge major versions",
+          "last 3 iOS major versions",
+          "last 3 Android major versions",
+          "last 2 ChromeAndroid major versions",
+          "Explorer 11"
+        ]
+      }
+    }],
+    "stage-2"
+  ]
+}

+ 2 - 15
.eslintrc.json

@@ -1,27 +1,14 @@
 {
 {
-  "extends": "standard",
+  "extends": "requarks",
   "env": {
   "env": {
     "node": true,
     "node": true,
     "es6": true,
     "es6": true,
     "jest": true
     "jest": true
   },
   },
   "globals": {
   "globals": {
-    // Client
     "document": false,
     "document": false,
     "navigator": false,
     "navigator": false,
     "window": false,
     "window": false,
-    "siteLang": false,
-    "socket": true,
-    "wikijs": true,
-    "FuseBox": false,
-    // Server
-    "appconfig": true,
-    "appdata": true,
-    "ROOTPATH": true,
-    "SERVERPATH": true,
-    "IS_DEBUG": true
-  },
-  "rules": {
-    "space-before-function-paren": 0
+    "FuseBox": false
   }
   }
 }
 }

+ 2 - 1
.npmrc

@@ -1 +1,2 @@
-save-prefix = "~"
+save-exact = true
+save-prefix = ""

+ 2 - 1
.yarnrc

@@ -1 +1,2 @@
-save-prefix "~"
+save-exact true
+save-prefix ""

+ 0 - 5
CHANGELOG.md

@@ -237,11 +237,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 ### Changed
 ### Changed
 - Updated dependencies + snyk policy
 - Updated dependencies + snyk policy
 
 
-[v1.0.11]: https://github.com/Requarks/wiki/releases/tag/v1.0.11
-[v1.0.10]: https://github.com/Requarks/wiki/releases/tag/v1.0.10
-[v1.0.9]: https://github.com/Requarks/wiki/releases/tag/v1.0.9
-[v1.0.8]: https://github.com/Requarks/wiki/releases/tag/v1.0.8
-[v1.0.7]: https://github.com/Requarks/wiki/releases/tag/v1.0.7
 [v1.0.6]: https://github.com/Requarks/wiki/releases/tag/v1.0.6
 [v1.0.6]: https://github.com/Requarks/wiki/releases/tag/v1.0.6
 [v1.0.5]: https://github.com/Requarks/wiki/releases/tag/v1.0.5
 [v1.0.5]: https://github.com/Requarks/wiki/releases/tag/v1.0.5
 [v1.0.4]: https://github.com/Requarks/wiki/releases/tag/v1.0.4
 [v1.0.4]: https://github.com/Requarks/wiki/releases/tag/v1.0.4

二进制
assets/images/bg.jpg


二进制
assets/images/bg_1.jpg


二进制
assets/images/bg_2.jpg


二进制
assets/images/bg_3.jpg


+ 8 - 0
assets/svg/auth-icon-azure.svg

@@ -0,0 +1,8 @@
+<svg width="100%" height="100%" viewBox="0 0 159 158" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+  <path fill="#51b5d7" d="M87.477,3.864c-4.583,-4.583 -12.024,-4.583 -16.608,0l-66.432,66.432c-4.583,4.583 -4.583,12.025 0,16.608l67.352,67.352c4.583,4.583 12.025,4.583 16.608,0l66.432,-66.432c4.583,-4.583 4.583,-12.025 0,-16.608l-67.352,-67.352Zm-14.477,44.282c-3.497,-2.176 -5.826,-6.054 -5.826,-10.472c0,-6.803 5.523,-12.326 12.326,-12.326c6.803,0 12.326,5.523 12.326,12.326c0,4.418 -2.329,8.296 -5.826,10.472l0,61.157c0.415,0.214 0.818,0.447 1.208,0.699l24.89,-24.89c-0.909,-1.718 -1.424,-3.676 -1.424,-5.753c0,-6.803 5.523,-12.326 12.326,-12.326c6.803,0 12.326,5.523 12.326,12.326c0,6.803 -5.523,12.326 -12.326,12.326c-1.493,0 -2.924,-0.266 -4.249,-0.753l-25.952,25.951c0.606,1.58 0.938,3.295 0.938,5.088c0,7.857 -6.38,14.236 -14.237,14.236c-7.857,0 -14.237,-6.379 -14.237,-14.236c0,-1.804 0.337,-3.531 0.95,-5.119l-26.466,-26.466c-1.182,0.377 -2.441,0.581 -3.747,0.581c-6.803,0 -12.326,-5.523 -12.326,-12.326c0,-6.803 5.523,-12.326 12.326,-12.326c6.803,0 12.326,5.523 12.326,12.326c0,2.254 -0.606,4.368 -1.665,6.187l25.157,25.157c0.382,-0.245 0.776,-0.473 1.182,-0.682l0,-61.157Z" />
+  <path fill="#97cbe1" d="M91.745,39.094l28.297,28.297c-4.527,1.116 -8.072,4.737 -9.079,9.306l-24.963,-24.963l0,-3.588c3.116,-1.939 5.306,-5.231 5.745,-9.052Zm-24.49,0c0.439,3.821 2.629,7.113 5.745,9.052l0,3.588l-24.846,24.846c-0.785,-4.655 -4.183,-8.427 -8.632,-9.753l27.733,-27.733Z" />
+  <circle fill="#97cbe1" cx="35.84" cy="78.701" r="6.701" />
+  <circle fill="#97cbe1" cx="123.16" cy="79.778" r="6.701" />
+  <circle fill="#97cbe1" cx="79.299" cy="37.243" r="6.701" />
+  <circle fill="#97cbe1" cx="79.633" cy="122.201" r="8.201" />
+</svg>

+ 3 - 0
assets/svg/auth-icon-facebook.svg

@@ -0,0 +1,3 @@
+<svg width="100%" height="100%" viewBox="0 0 500 500" preserveAspectRatio="xMinYMin meet">
+	<path fill="#3B5998" d="M288.714,500l0,-228.073l76.554,0l11.461,-88.885l-88.017,0l0,-56.749c0,-25.735 7.145,-43.271 44.049,-43.271l47.067,-0.022l0,-79.498c-8.141,-1.081 -36.082,-3.502 -68.584,-3.502c-67.862,0 -114.321,41.422 -114.321,117.492l0,65.55l-76.751,0l0,88.885l76.751,0l0,228.071l91.791,0l0,0.002Z" style="fill-rule:nonzero;"/>
+</svg>

文件差异内容过多而无法显示
+ 0 - 0
assets/svg/auth-icon-github.svg


+ 1 - 0
assets/svg/auth-icon-google.svg

@@ -0,0 +1 @@
+<svg width="2443" height="2500" viewBox="0 0 256 262" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" fill="#4285F4"/><path d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" fill="#34A853"/><path d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" fill="#FBBC05"/><path d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" fill="#EB4335"/></svg>

+ 10 - 0
assets/svg/auth-icon-ldap.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="48px" height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve">
+<path fill="#458BC4" d="M44.804,30.404l-20-27C24.615,3.15,24.308,3.023,24,3.023V46c0.2,0,0.401-0.061,0.573-0.181l20-14
+	c0.222-0.155,0.37-0.393,0.414-0.659C45.03,30.895,44.964,30.622,44.804,30.404z"/>
+<path fill="#43A6DD" d="M23.196,3.405l-20,27c-0.16,0.218-0.227,0.49-0.184,0.756c0.044,0.267,0.192,0.504,0.414,0.659l20,14
+	C23.599,45.939,23.8,46,24,46V3.023C23.692,3.023,23.385,3.15,23.196,3.405z"/>
+</svg>

+ 8 - 0
assets/svg/auth-icon-local.svg

@@ -0,0 +1,8 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
+<path d="M32,19c13.089,0,27-3.154,27-9S45.089,1,32,1S5,4.154,5,10S18.911,19,32,19z"/>
+<path d="M32,41c13.089,0,27-3.154,27-9V14.436c-1.481,1.493-3.963,2.968-8.022,4.174C45.864,20.128,38.946,21,32,21
+	s-13.864-0.872-18.978-2.391C8.963,17.403,6.481,15.929,5,14.436V32C5,37.846,18.911,41,32,41z"/>
+<path d="M32,63c13.089,0,27-3.154,27-9V36.436c-1.481,1.493-3.963,2.968-8.022,4.174C45.864,42.128,38.946,43,32,43
+	s-13.864-0.872-18.978-2.391C8.963,39.403,6.481,37.929,5,36.436V54C5,59.846,18.911,63,32,63z"/>
+</svg>

+ 1 - 0
assets/svg/auth-icon-microsoft.svg

@@ -0,0 +1 @@
+<svg width="2490" height="2500" viewBox="0 0 256 257" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M0 36.357L104.62 22.11l.045 100.914-104.57.595L0 36.358zm104.57 98.293l.08 101.002L.081 221.275l-.006-87.302 104.494.677zm12.682-114.405L255.968 0v121.74l-138.716 1.1V20.246zM256 135.6l-.033 121.191-138.716-19.578-.194-101.84L256 135.6z" fill="#00ADEF"/></svg>

+ 1 - 0
assets/svg/auth-icon-slack.svg

@@ -0,0 +1 @@
+<svg width="2500" height="2500" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M165.964 15.838c-3.89-11.975-16.752-18.528-28.725-14.636-11.975 3.89-18.528 16.752-14.636 28.725l58.947 181.365c4.048 11.187 16.132 17.473 27.732 14.135 12.1-3.483 19.475-16.334 15.614-28.217L165.964 15.838" fill="#DFA22F"/><path d="M74.626 45.516C70.734 33.542 57.873 26.989 45.9 30.879 33.924 34.77 27.37 47.631 31.263 59.606l58.948 181.366c4.047 11.186 16.132 17.473 27.732 14.132 12.099-3.481 19.474-16.332 15.613-28.217L74.626 45.516" fill="#3CB187"/><path d="M240.162 166.045c11.975-3.89 18.526-16.75 14.636-28.726-3.89-11.973-16.752-18.527-28.725-14.636L44.708 181.632c-11.187 4.046-17.473 16.13-14.135 27.73 3.483 12.099 16.334 19.475 28.217 15.614l181.372-58.93" fill="#CE1E5B"/><path d="M82.508 217.27l43.347-14.084-14.086-43.352-43.35 14.09 14.089 43.347" fill="#392538"/><path d="M173.847 187.591c16.388-5.323 31.62-10.273 43.348-14.084l-14.088-43.36-43.35 14.09 14.09 43.354" fill="#BB242A"/><path d="M210.484 74.706c11.974-3.89 18.527-16.751 14.637-28.727-3.89-11.973-16.752-18.526-28.727-14.636L15.028 90.293C3.842 94.337-2.445 106.422.896 118.022c3.481 12.098 16.332 19.474 28.217 15.613l181.371-58.93" fill="#72C5CD"/><path d="M52.822 125.933c11.805-3.836 27.025-8.782 43.354-14.086-5.323-16.39-10.273-31.622-14.084-43.352l-43.36 14.092 14.09 43.346" fill="#248C73"/><path d="M144.16 96.256l43.356-14.088a546179.21 546179.21 0 0 0-14.089-43.36L130.07 52.9l14.09 43.356" fill="#62803A"/></svg>

+ 7 - 0
assets/svg/login-bg-dark.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1500 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+    <path d="M1500,543c0,0 -135.753,54.677 -252.197,101.577c-106.023,42.702 -223.596,47.2 -332.571,12.724c-56.088,-17.745 -118.404,-37.46 -173.999,-55.048c-103.773,-32.831 -216.376,-22.709 -312.631,28.103c0,0 0,0.001 0,0.001c-94.821,50.055 -206.441,57.476 -307.053,20.415c-63.317,-23.322 -121.549,-44.772 -121.549,-44.772l0,417l1500,0l0,-480Z" style="fill:#2d2d2d;fill-opacity:0.282609;"/>
+    <path d="M1510,580c0,0 -144.155,47.882 -252.311,83.806c-74.651,24.796 -156.199,17.958 -225.679,-18.923c0,0 0,0 0,0c-62.207,-33.021 -133.629,-44.415 -203.023,-32.389c-98.381,17.051 -244.859,42.438 -352.664,61.121c-92.259,15.99 -187.076,8.079 -275.41,-22.977c-93.342,-32.818 -200.913,-70.638 -200.913,-70.638l0,466l1500,0l10,-466Z" style="fill:#2d2d2d;fill-opacity:0.550725;"/>
+    <path d="M1500,650c0,0 -143.367,28.581 -239.425,47.731c-56.087,11.181 -113.694,12.508 -170.237,3.922c-74.75,-11.351 -183.318,-27.838 -261.719,-39.743c-65.252,-9.909 -131.707,-8.759 -196.577,3.4c-49.655,9.308 -109.704,20.564 -158.992,29.803c-63.125,11.833 -127.839,12.479 -191.188,1.911c-111.875,-18.665 -281.862,-47.024 -281.862,-47.024l0,430l1500,0l0,-430Z" style="fill:#2d2d2d;"/>
+</svg>

+ 7 - 0
assets/svg/login-bg.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 1500 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
+    <path d="M1500,543c0,0 -135.753,54.677 -252.197,101.577c-106.023,42.702 -223.596,47.2 -332.571,12.724c-56.088,-17.745 -118.404,-37.46 -173.999,-55.048c-103.773,-32.831 -216.376,-22.709 -312.631,28.103c0,0 0,0.001 0,0.001c-94.821,50.055 -206.441,57.476 -307.053,20.415c-63.317,-23.322 -121.549,-44.772 -121.549,-44.772l0,417l1500,0l0,-480Z" style="fill:#fff;fill-opacity:0.282609;"/>
+    <path d="M1510,580c0,0 -144.155,47.882 -252.311,83.806c-74.651,24.796 -156.199,17.958 -225.679,-18.923c0,0 0,0 0,0c-62.207,-33.021 -133.629,-44.415 -203.023,-32.389c-98.381,17.051 -244.859,42.438 -352.664,61.121c-92.259,15.99 -187.076,8.079 -275.41,-22.977c-93.342,-32.818 -200.913,-70.638 -200.913,-70.638l0,466l1500,0l10,-466Z" style="fill:#fff;fill-opacity:0.550725;"/>
+    <path d="M1500,650c0,0 -143.367,28.581 -239.425,47.731c-56.087,11.181 -113.694,12.508 -170.237,3.922c-74.75,-11.351 -183.318,-27.838 -261.719,-39.743c-65.252,-9.909 -131.707,-8.759 -196.577,3.4c-49.655,9.308 -109.704,20.564 -158.992,29.803c-63.125,11.833 -127.839,12.479 -191.188,1.911c-111.875,-18.665 -281.862,-47.024 -281.862,-47.024l0,430l1500,0l0,-430Z" style="fill:#fff;"/>
+</svg>

+ 0 - 4
client/configure.js

@@ -1,4 +0,0 @@
-'use strict'
-
-require('./scss/configure.scss')
-require('./js/configure.js')

+ 2 - 15
client/index.js

@@ -1,17 +1,4 @@
 'use strict'
 'use strict'
 
 
-let logic = document.documentElement.dataset.logic
-
-switch (logic) {
-  case 'error':
-    require('./scss/error.scss')
-    break
-  case 'login':
-    require('./scss/login.scss')
-    require('./js/login.js')
-    break
-  default:
-    require('./scss/app.scss')
-    require('./js/app.js')
-    break
-}
+require('./scss/app.scss')
+require('./js/app.js')

+ 51 - 47
client/js/app.js

@@ -1,28 +1,28 @@
 'use strict'
 'use strict'
 
 
-/* global $, siteRoot */
+/* global siteConfig */
 /* eslint-disable no-new */
 /* eslint-disable no-new */
 
 
+import CONSTANTS from './constants'
+
 import Vue from 'vue'
 import Vue from 'vue'
 import VueResource from 'vue-resource'
 import VueResource from 'vue-resource'
 import VueClipboards from 'vue-clipboards'
 import VueClipboards from 'vue-clipboards'
-import VueLodash from 'vue-lodash'
+import VeeValidate from 'vee-validate'
+import { ApolloClient, createBatchingNetworkInterface } from 'apollo-client'
 import store from './store'
 import store from './store'
-import io from 'socket-io-client'
-import i18next from 'i18next'
-import i18nextXHR from 'i18next-xhr-backend'
-import VueI18Next from '@panter/vue-i18next'
-import 'jquery-contextmenu'
-import 'jquery-simple-upload'
-import 'jquery-smooth-scroll'
-import 'jquery-sticky'
+
+// ====================================
+// Load Modules
+// ====================================
+
+import localization from './modules/localization'
 
 
 // ====================================
 // ====================================
 // Load Helpers
 // Load Helpers
 // ====================================
 // ====================================
 
 
 import helpers from './helpers'
 import helpers from './helpers'
-import _ from './helpers/lodash'
 
 
 // ====================================
 // ====================================
 // Load Vue Components
 // Load Vue Components
@@ -36,6 +36,7 @@ import editorFileComponent from './components/editor-file.vue'
 import editorVideoComponent from './components/editor-video.vue'
 import editorVideoComponent from './components/editor-video.vue'
 import historyComponent from './components/history.vue'
 import historyComponent from './components/history.vue'
 import loadingSpinnerComponent from './components/loading-spinner.vue'
 import loadingSpinnerComponent from './components/loading-spinner.vue'
+import loginComponent from './components/login.vue'
 import modalCreatePageComponent from './components/modal-create-page.vue'
 import modalCreatePageComponent from './components/modal-create-page.vue'
 import modalCreateUserComponent from './components/modal-create-user.vue'
 import modalCreateUserComponent from './components/modal-create-user.vue'
 import modalDeletePageComponent from './components/modal-delete-page.vue'
 import modalDeletePageComponent from './components/modal-delete-page.vue'
@@ -53,19 +54,48 @@ import adminEditUserComponent from './pages/admin-edit-user.component.js'
 import adminProfileComponent from './pages/admin-profile.component.js'
 import adminProfileComponent from './pages/admin-profile.component.js'
 import adminSettingsComponent from './pages/admin-settings.component.js'
 import adminSettingsComponent from './pages/admin-settings.component.js'
 import adminThemeComponent from './pages/admin-theme.component.js'
 import adminThemeComponent from './pages/admin-theme.component.js'
+import configManagerComponent from './components/config-manager.component.js'
 import contentViewComponent from './pages/content-view.component.js'
 import contentViewComponent from './pages/content-view.component.js'
 import editorComponent from './components/editor.component.js'
 import editorComponent from './components/editor.component.js'
 import sourceViewComponent from './pages/source-view.component.js'
 import sourceViewComponent from './pages/source-view.component.js'
 
 
+// ====================================
+// Initialize Global Vars
+// ====================================
+
+window.wiki = null
+window.CONSTANTS = CONSTANTS
+
+// ====================================
+// Initialize Apollo Client (GraphQL)
+// ====================================
+
+window.graphQL = new ApolloClient({
+  networkInterface: createBatchingNetworkInterface({
+    uri: window.location.protocol + '//' + window.location.host + siteConfig.path + '/graphql'
+  }),
+  connectToDevTools: true
+})
+
 // ====================================
 // ====================================
 // Initialize Vue Modules
 // Initialize Vue Modules
 // ====================================
 // ====================================
 
 
 Vue.use(VueResource)
 Vue.use(VueResource)
 Vue.use(VueClipboards)
 Vue.use(VueClipboards)
-Vue.use(VueI18Next)
-Vue.use(VueLodash, _)
+Vue.use(localization.VueI18Next)
 Vue.use(helpers)
 Vue.use(helpers)
+Vue.use(VeeValidate, {
+  enableAutoClasses: true,
+  classNames: {
+    touched: 'is-touched', // the control has been blurred
+    untouched: 'is-untouched', // the control hasn't been blurred
+    valid: 'is-valid', // model is valid
+    invalid: 'is-invalid', // model is invalid
+    pristine: 'is-pristine', // control has not been interacted with
+    dirty: 'is-dirty' // control has been interacted with
+  }
+})
 
 
 // ====================================
 // ====================================
 // Register Vue Components
 // Register Vue Components
@@ -78,6 +108,7 @@ Vue.component('adminSettings', adminSettingsComponent)
 Vue.component('adminTheme', adminThemeComponent)
 Vue.component('adminTheme', adminThemeComponent)
 Vue.component('anchor', anchorComponent)
 Vue.component('anchor', anchorComponent)
 Vue.component('colorPicker', colorPickerComponent)
 Vue.component('colorPicker', colorPickerComponent)
+Vue.component('configManager', configManagerComponent)
 Vue.component('contentView', contentViewComponent)
 Vue.component('contentView', contentViewComponent)
 Vue.component('editor', editorComponent)
 Vue.component('editor', editorComponent)
 Vue.component('editorCodeblock', editorCodeblockComponent)
 Vue.component('editorCodeblock', editorCodeblockComponent)
@@ -85,6 +116,7 @@ Vue.component('editorFile', editorFileComponent)
 Vue.component('editorVideo', editorVideoComponent)
 Vue.component('editorVideo', editorVideoComponent)
 Vue.component('history', historyComponent)
 Vue.component('history', historyComponent)
 Vue.component('loadingSpinner', loadingSpinnerComponent)
 Vue.component('loadingSpinner', loadingSpinnerComponent)
+Vue.component('login', loginComponent)
 Vue.component('modalCreatePage', modalCreatePageComponent)
 Vue.component('modalCreatePage', modalCreatePageComponent)
 Vue.component('modalCreateUser', modalCreateUserComponent)
 Vue.component('modalCreateUser', modalCreateUserComponent)
 Vue.component('modalDeletePage', modalDeletePageComponent)
 Vue.component('modalDeletePage', modalDeletePageComponent)
@@ -99,52 +131,26 @@ Vue.component('sourceView', sourceViewComponent)
 Vue.component('toggle', toggleComponent)
 Vue.component('toggle', toggleComponent)
 Vue.component('tree', treeComponent)
 Vue.component('tree', treeComponent)
 
 
-// ====================================
-// Load Localization strings
-// ====================================
-
-i18next
-  .use(i18nextXHR)
-  .init({
-    backend: {
-      loadPath: siteRoot + '/js/i18n/{{lng}}.json'
-    },
-    lng: siteLang,
-    fallbackLng: siteLang
-  })
-
-$(() => {
+document.addEventListener('DOMContentLoaded', ev => {
   // ====================================
   // ====================================
   // Notifications
   // Notifications
   // ====================================
   // ====================================
 
 
-  $(window).bind('beforeunload', () => {
+  window.addEventListener('beforeunload', () => {
     store.dispatch('startLoading')
     store.dispatch('startLoading')
   })
   })
-  $(document).ajaxSend(() => {
-    store.dispatch('startLoading')
-  }).ajaxComplete(() => {
-    store.dispatch('stopLoading')
-  })
-
-  // ====================================
-  // Establish WebSocket connection
-  // ====================================
-
-  let socket = io(window.location.origin)
-  window.socket = socket
 
 
   // ====================================
   // ====================================
   // Bootstrap Vue
   // Bootstrap Vue
   // ====================================
   // ====================================
 
 
-  const i18n = new VueI18Next(i18next)
-  window.wikijs = new Vue({
+  const i18n = localization.init()
+  window.wiki = new Vue({
     mixins: [helpers],
     mixins: [helpers],
     components: {},
     components: {},
     store,
     store,
     i18n,
     i18n,
-    el: '#root',
+    el: '#app',
     methods: {
     methods: {
       changeTheme(opts) {
       changeTheme(opts) {
         this.$el.className = `has-stickynav is-primary-${opts.primary} is-alternate-${opts.alt}`
         this.$el.className = `has-stickynav is-primary-${opts.primary} is-alternate-${opts.alt}`
@@ -153,9 +159,7 @@ $(() => {
       }
       }
     },
     },
     mounted() {
     mounted() {
-      $('a:not(.toc-anchor)').smoothScroll({ speed: 500, offset: -50 })
-      $('#header').sticky({ topSpacing: 0 })
-      $('.sidebar-pagecontents').sticky({ topSpacing: 15, bottomSpacing: 75 })
+
     }
     }
   })
   })
 })
 })

+ 153 - 0
client/js/app.old.js

@@ -0,0 +1,153 @@
+'use strict'
+
+/* global $, siteConfig */
+/* eslint-disable no-new */
+
+import Vue from 'vue'
+import VueResource from 'vue-resource'
+import VueClipboards from 'vue-clipboards'
+import VueLodash from 'vue-lodash'
+import store from './store'
+import i18next from 'i18next'
+import i18nextXHR from 'i18next-xhr-backend'
+import VueI18Next from '@panter/vue-i18next'
+import 'jquery-contextmenu'
+import 'jquery-simple-upload'
+import 'jquery-smooth-scroll'
+import 'jquery-sticky'
+
+// ====================================
+// Load Helpers
+// ====================================
+
+import helpers from './helpers'
+import _ from './helpers/lodash'
+
+// ====================================
+// Load Vue Components
+// ====================================
+
+import alertComponent from './components/alert.vue'
+import anchorComponent from './components/anchor.vue'
+import colorPickerComponent from './components/color-picker.vue'
+import editorCodeblockComponent from './components/editor-codeblock.vue'
+import editorFileComponent from './components/editor-file.vue'
+import editorVideoComponent from './components/editor-video.vue'
+import historyComponent from './components/history.vue'
+import loadingSpinnerComponent from './components/loading-spinner.vue'
+import modalCreatePageComponent from './components/modal-create-page.vue'
+import modalCreateUserComponent from './components/modal-create-user.vue'
+import modalDeleteUserComponent from './components/modal-delete-user.vue'
+import modalDiscardPageComponent from './components/modal-discard-page.vue'
+import modalMovePageComponent from './components/modal-move-page.vue'
+import modalProfile2faComponent from './components/modal-profile-2fa.vue'
+import modalUpgradeSystemComponent from './components/modal-upgrade-system.vue'
+import pageLoaderComponent from './components/page-loader.vue'
+import searchComponent from './components/search.vue'
+import toggleComponent from './components/toggle.vue'
+import treeComponent from './components/tree.vue'
+
+import adminEditUserComponent from './pages/admin-edit-user.component.js'
+import adminProfileComponent from './pages/admin-profile.component.js'
+import adminSettingsComponent from './pages/admin-settings.component.js'
+import adminThemeComponent from './pages/admin-theme.component.js'
+import contentViewComponent from './pages/content-view.component.js'
+import editorComponent from './components/editor.component.js'
+import sourceViewComponent from './pages/source-view.component.js'
+
+// ====================================
+// Initialize Vue Modules
+// ====================================
+
+Vue.use(VueResource)
+Vue.use(VueClipboards)
+Vue.use(VueI18Next)
+Vue.use(VueLodash, _)
+Vue.use(helpers)
+
+// ====================================
+// Register Vue Components
+// ====================================
+
+Vue.component('alert', alertComponent)
+Vue.component('adminEditUser', adminEditUserComponent)
+Vue.component('adminProfile', adminProfileComponent)
+Vue.component('adminSettings', adminSettingsComponent)
+Vue.component('adminTheme', adminThemeComponent)
+Vue.component('anchor', anchorComponent)
+Vue.component('colorPicker', colorPickerComponent)
+Vue.component('contentView', contentViewComponent)
+Vue.component('editor', editorComponent)
+Vue.component('editorCodeblock', editorCodeblockComponent)
+Vue.component('editorFile', editorFileComponent)
+Vue.component('editorVideo', editorVideoComponent)
+Vue.component('history', historyComponent)
+Vue.component('loadingSpinner', loadingSpinnerComponent)
+Vue.component('modalCreatePage', modalCreatePageComponent)
+Vue.component('modalCreateUser', modalCreateUserComponent)
+Vue.component('modalDeleteUser', modalDeleteUserComponent)
+Vue.component('modalDiscardPage', modalDiscardPageComponent)
+Vue.component('modalMovePage', modalMovePageComponent)
+Vue.component('modalProfile2fa', modalProfile2faComponent)
+Vue.component('modalUpgradeSystem', modalUpgradeSystemComponent)
+Vue.component('pageLoader', pageLoaderComponent)
+Vue.component('search', searchComponent)
+Vue.component('sourceView', sourceViewComponent)
+Vue.component('toggle', toggleComponent)
+Vue.component('tree', treeComponent)
+
+// ====================================
+// Load Localization strings
+// ====================================
+
+i18next
+  .use(i18nextXHR)
+  .init({
+    backend: {
+      loadPath: siteConfig.path + '/js/i18n/{{lng}}.json'
+    },
+    lng: siteConfig.lang,
+    fallbackLng: siteConfig.lang
+  })
+
+$(() => {
+  // ====================================
+  // Notifications
+  // ====================================
+
+  $(window).bind('beforeunload', () => {
+    store.dispatch('startLoading')
+  })
+  $(document).ajaxSend(() => {
+    store.dispatch('startLoading')
+  }).ajaxComplete(() => {
+    store.dispatch('stopLoading')
+  })
+
+  // ====================================
+  // Bootstrap Vue
+  // ====================================
+
+  const i18n = new VueI18Next(i18next)
+  if (document.querySelector('#root')) {
+    window.wikijs = new Vue({
+      mixins: [helpers],
+      components: {},
+      store,
+      i18n,
+      el: '#root',
+      methods: {
+        changeTheme(opts) {
+          this.$el.className = `has-stickynav is-primary-${opts.primary} is-alternate-${opts.alt}`
+          this.$refs.header.className = `nav is-${opts.primary}`
+          this.$refs.footer.className = `footer is-${opts.footer}`
+        }
+      },
+      mounted() {
+        $('a:not(.toc-anchor)').smoothScroll({ speed: 500, offset: -50 })
+        $('#header').sticky({ topSpacing: 0 })
+        $('.sidebar-pagecontents').sticky({ topSpacing: 15, bottomSpacing: 75 })
+      }
+    })
+  }
+})

+ 239 - 0
client/js/components/config-manager.component.js

@@ -0,0 +1,239 @@
+/* global siteConfig */
+
+import axios from 'axios'
+
+export default {
+  name: 'configManager',
+  data() {
+    return {
+      loading: false,
+      state: 'welcome',
+      syscheck: {
+        ok: false,
+        error: '',
+        results: []
+      },
+      dbcheck: {
+        ok: false,
+        error: ''
+      },
+      gitcheck: {
+        ok: false,
+        error: ''
+      },
+      final: {
+        ok: false,
+        error: '',
+        results: []
+      },
+      conf: {
+        telemetry: true,
+        upgrade: false,
+        title: siteConfig.title || 'Wiki',
+        host: siteConfig.host || 'http://',
+        port: siteConfig.port || 80,
+        lang: siteConfig.lang || 'en',
+        public: (siteConfig.public === true),
+        pathData: './data',
+        pathRepo: './repo',
+        gitUseRemote: (siteConfig.git !== false),
+        gitUrl: '',
+        gitBranch: 'master',
+        gitAuthType: 'ssh',
+        gitAuthSSHKey: '',
+        gitAuthUser: '',
+        gitAuthPass: '',
+        gitAuthSSL: true,
+        gitShowUserEmail: true,
+        gitServerEmail: '',
+        adminEmail: '',
+        adminPassword: '',
+        adminPasswordConfirm: ''
+      },
+      considerations: {
+        https: false,
+        port: false,
+        localhost: false
+      }
+    }
+  },
+  computed: {
+    currentProgress: function () {
+      let perc = '0%'
+      switch (this.state) {
+        case 'welcome':
+          perc = '0%'
+          break
+        case 'syscheck':
+          perc = (this.syscheck.ok) ? '15%' : '5%'
+          break
+        case 'general':
+          perc = '25%'
+          break
+        case 'considerations':
+          perc = '30%'
+          break
+        case 'git':
+          perc = '50%'
+          break
+        case 'gitcheck':
+          perc = (this.gitcheck.ok) ? '70%' : '55%'
+          break
+        case 'admin':
+          perc = '75%'
+          break
+      }
+      return perc
+    }
+  },
+  mounted: function () {
+    /* if (appconfig.paths) {
+      this.conf.pathData = appconfig.paths.data || './data'
+      this.conf.pathRepo = appconfig.paths.repo || './repo'
+    }
+    if (appconfig.git !== false && _.isPlainObject(appconfig.git)) {
+      this.conf.gitUrl = appconfig.git.url || ''
+      this.conf.gitBranch = appconfig.git.branch || 'master'
+      this.conf.gitShowUserEmail = (appconfig.git.showUserEmail !== false)
+      this.conf.gitServerEmail = appconfig.git.serverEmail || ''
+      if (_.isPlainObject(appconfig.git.auth)) {
+        this.conf.gitAuthType = appconfig.git.auth.type || 'ssh'
+        this.conf.gitAuthSSHKey = appconfig.git.auth.privateKey || ''
+        this.conf.gitAuthUser = appconfig.git.auth.username || ''
+        this.conf.gitAuthPass = appconfig.git.auth.password || ''
+        this.conf.gitAuthSSL = (appconfig.git.auth.sslVerify !== false)
+      }
+    } */
+  },
+  methods: {
+    proceedToWelcome: function (ev) {
+      this.state = 'welcome'
+      this.loading = false
+    },
+    proceedToSyscheck: function (ev) {
+      let self = this
+      this.state = 'syscheck'
+      this.loading = true
+      self.syscheck = {
+        ok: false,
+        error: '',
+        results: []
+      }
+
+      this.$helpers._.delay(() => {
+        axios.post('/syscheck').then(resp => {
+          if (resp.data.ok === true) {
+            self.syscheck.ok = true
+            self.syscheck.results = resp.data.results
+          } else {
+            self.syscheck.ok = false
+            self.syscheck.error = resp.data.error
+          }
+          self.loading = false
+          self.$nextTick()
+        }).catch(err => {
+          window.alert(err.message)
+        })
+      }, 1000)
+    },
+    proceedToGeneral: function (ev) {
+      let self = this
+      self.state = 'general'
+      self.loading = false
+      self.$nextTick(() => {
+        self.$validator.validateAll('general')
+      })
+    },
+    proceedToConsiderations: function (ev) {
+      this.considerations = {
+        https: !this.$helpers._.startsWith(this.conf.host, 'https'),
+        port: false, // TODO
+        localhost: this.$helpers._.includes(this.conf.host, 'localhost')
+      }
+      this.state = 'considerations'
+      this.loading = false
+    },
+    proceedToGit: function (ev) {
+      let self = this
+      self.state = 'git'
+      self.loading = false
+      self.$nextTick(() => {
+        self.$validator.validateAll('git')
+      })
+    },
+    proceedToGitCheck: function (ev) {
+      let self = this
+      this.state = 'gitcheck'
+      this.loading = true
+      self.gitcheck = {
+        ok: false,
+        results: [],
+        error: ''
+      }
+
+      this.$helpers._.delay(() => {
+        axios.post('/gitcheck', self.conf).then(resp => {
+          if (resp.data.ok === true) {
+            self.gitcheck.ok = true
+            self.gitcheck.results = resp.data.results
+          } else {
+            self.gitcheck.ok = false
+            self.gitcheck.error = resp.data.error
+          }
+          self.loading = false
+          self.$nextTick()
+        }).catch(err => {
+          window.alert(err.message)
+        })
+      }, 1000)
+    },
+    proceedToAdmin: function (ev) {
+      let self = this
+      self.state = 'admin'
+      self.loading = false
+      self.$nextTick(() => {
+        self.$validator.validateAll('admin')
+      })
+    },
+    proceedToFinal: function (ev) {
+      let self = this
+      self.state = 'final'
+      self.loading = true
+      self.final = {
+        ok: false,
+        error: '',
+        results: []
+      }
+
+      this.$helpers._.delay(() => {
+        axios.post('/finalize', self.conf).then(resp => {
+          if (resp.data.ok === true) {
+            self.final.ok = true
+            self.final.results = resp.data.results
+          } else {
+            self.final.ok = false
+            self.final.error = resp.data.error
+          }
+          self.loading = false
+          self.$nextTick()
+        }).catch(err => {
+          window.alert(err.message)
+        })
+      }, 1000)
+    },
+    finish: function (ev) {
+      let self = this
+      self.state = 'restart'
+
+      this.$helpers._.delay(() => {
+        axios.post('/restart', {}).then(resp => {
+          this.$helpers._.delay(() => {
+            window.location.assign(self.conf.host)
+          }, 30000)
+        }).catch(err => {
+          window.alert(err.message)
+        })
+      }, 1000)
+    }
+  }
+}

+ 3 - 3
client/js/components/editor-video.vue

@@ -34,9 +34,9 @@
 
 
 <script>
 <script>
   const videoRules = {
   const videoRules = {
-    'youtube': new RegExp(/(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/, 'i'),
-    'vimeo': new RegExp(/vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^/]*)\/videos\/|album\/(?:\d+)\/video\/|)(\d+)(?:$|\/|\?)/, 'i'),
-    'dailymotion': new RegExp(/(?:dailymotion\.com(?:\/embed)?(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/, 'i')
+    'youtube': new RegExp('/(?:(?:youtu\\.be\\/|v\\/|vi\\/|u\\/\\w\\/|embed\\/)|(?:(?:watch)?\\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/', 'i'),
+    'vimeo': new RegExp('/vimeo.com\\/(?:channels\\/(?:\\w+\\/)?|groups\\/(?:[^/]*)\\/videos\\/|album\\/(?:\\d+)\\/video\\/|)(\\d+)(?:$|\\/|\\?)/', 'i'),
+    'dailymotion': new RegExp('/(?:dailymotion\\.com(?:\\/embed)?(?:\\/video|\\/hub)|dai\\.ly)\\/([0-9a-z]+)(?:[-_0-9a-zA-Z]+(?:#video=)?([a-z0-9]+)?)?/', 'i')
   }
   }
 
 
   export default {
   export default {

+ 70 - 0
client/js/components/login.vue

@@ -0,0 +1,70 @@
+<template lang="pug">
+  .login(:class='{ "is-error": error }')
+    .login-container(:class='{ "is-expanded": strategies.length > 1 }')
+      .login-error(v-if='error')
+        strong
+          i.icon-warning-outline
+          | {{ error.title }}
+        span {{ error.message }}
+      .login-providers(v-show='strategies.length > 1')
+        button(v-for='strategy in strategies', :class='{ "is-active": strategy.key === selectedStrategy }', @click='selectStrategy(strategy.key, strategy.useForm)', :title='strategy.title')
+          em(v-html='strategy.icon')
+          span {{ strategy.title }}
+      .login-frame
+        h1 {{ siteTitle }}
+        h2 {{ $t('auth:loginrequired') }}
+        form(method='post', action='/login')
+          input#login-user(type='text', name='email', :placeholder='$t("auth:fields.emailuser")')
+          input#login-pass(type='password', name='password', :placeholder='$t("auth:fields.password")')
+          button.button.is-light-blue.is-fullwidth(type='submit')
+            span {{ $t('auth:actions.login') }}
+    .login-copyright
+      span {{ $t('footer.poweredby') }}
+      a(href='https://wiki.js.org', rel='external', title='Wiki.js') Wiki.js
+</template>
+
+<script>
+export default {
+  name: 'login',
+  data() {
+    return {
+      error: false,
+      strategies: [],
+      selectedStrategy: 'local'
+    }
+  },
+  computed: {
+    siteTitle() {
+      return siteConfig.title
+    }
+  },
+  methods: {
+    selectStrategy(key, useForm) {
+      this.selectedStrategy = key
+      if (!useForm) {
+        window.location.assign(siteConfig.path + '/login/' + key)
+      }
+    },
+    refreshStrategies() {
+      graphQL.query({
+        query: CONSTANTS.GRAPHQL.GQL_QUERY_AUTHENTICATION,
+        variables: {
+          mode: 'active'
+        }
+      }).then(resp => {
+        if (resp.data.authentication) {
+          this.strategies = resp.data.authentication
+        } else {
+          throw new Error('No authentication providers available!')
+        }
+      }).catch(err => {
+        console.error(err)
+      })
+    }
+  },
+  mounted() {
+    this.refreshStrategies()
+  }
+}
+</script>
+

+ 13 - 14
client/js/components/page-loader.vue

@@ -5,20 +5,19 @@
       span {{ msg }}
       span {{ msg }}
 </template>
 </template>
 
 
-<script>
-  export default {
-    name: 'page-loader',
-    props: ['text'],
-    data () {
-      return {}
-    },
-    computed: {
-      msg () { return this.$store.state.pageLoader.msg },
-      isShown () { return this.$store.state.pageLoader.shown }
-    },
-    mounted() {
-      this.$store.commit('pageLoader/msgChange', this.text)
-    }
+<script type='js'>
+export default {
+  name: 'page-loader',
+  props: ['text'],
+  data () {
+    return {}
+  },
+  computed: {
+    msg () { return this.$store.state.pageLoader.msg },
+    isShown () { return this.$store.state.pageLoader.shown }
+  },
+  mounted() {
+    this.$store.commit('pageLoader/msgChange', this.text)
   }
   }
 }
 }
 </script>
 </script>

+ 22 - 0
client/js/constants/graphql.js

@@ -0,0 +1,22 @@
+import gql from 'graphql-tag'
+
+export default {
+  GQL_QUERY_AUTHENTICATION: gql`
+    query($mode: String!) {
+      authentication(mode:$mode) {
+        key
+        useForm
+        title
+        icon
+      }
+    }
+  `,
+  GQL_QUERY_TRANSLATIONS: gql`
+    query($locale: String!, $namespace: String!) {
+      translations(locale:$locale, namespace:$namespace) {
+        key
+        value
+      }
+    }
+  `
+}

+ 5 - 0
client/js/constants/index.js

@@ -0,0 +1,5 @@
+import GRAPHQL from './graphql'
+
+export default {
+  GRAPHQL
+}

+ 1 - 0
client/js/helpers/index.js

@@ -1,6 +1,7 @@
 'use strict'
 'use strict'
 
 
 const helpers = {
 const helpers = {
+  _: require('./lodash'),
   common: require('./common'),
   common: require('./common'),
   form: require('./form'),
   form: require('./form'),
   pages: require('./pages')
   pages: require('./pages')

+ 0 - 7
client/js/login.js

@@ -1,7 +0,0 @@
-'use strict'
-
-/* global $ */
-
-$(() => {
-  $('#login-user').focus()
-})

+ 52 - 0
client/js/modules/localization.js

@@ -0,0 +1,52 @@
+import i18next from 'i18next'
+import i18nextXHR from 'i18next-xhr-backend'
+import i18nextCache from 'i18next-localstorage-cache'
+import VueI18Next from '@panter/vue-i18next'
+import loSet from 'lodash/set'
+
+/* global siteConfig, graphQL, CONSTANTS */
+
+module.exports = {
+  VueI18Next,
+  init() {
+    i18next
+      .use(i18nextXHR)
+      .use(i18nextCache)
+      .init({
+        backend: {
+          loadPath: '{{lng}}/{{ns}}',
+          parse: (data) => data,
+          ajax: (url, opts, cb, data) => {
+            let langParams = url.split('/')
+            graphQL.query({
+              query: CONSTANTS.GRAPHQL.GQL_QUERY_TRANSLATIONS,
+              variables: {
+                locale: langParams[0],
+                namespace: langParams[1]
+              }
+            }).then(resp => {
+              let ns = {}
+              if (resp.data.translations.length > 0) {
+                resp.data.translations.forEach(entry => {
+                  loSet(ns, entry.key, entry.value)
+                })
+              }
+              return cb(ns, {status: '200'})
+            }).catch(err => {
+              console.error(err)
+              return cb(null, {status: '404'})
+            })
+          }
+        },
+        cache: {
+          enabled: true,
+          expiration: 60 * 60 * 1000
+        },
+        defaultNS: 'common',
+        lng: siteConfig.lang,
+        fallbackLng: siteConfig.lang,
+        ns: ['common', 'admin', 'auth']
+      })
+    return new VueI18Next(i18next)
+  }
+}

+ 3 - 1
client/scss/app.scss

@@ -15,6 +15,7 @@ $primary: 'indigo';
 @import 'components/button';
 @import 'components/button';
 @import 'components/collapsable-nav';
 @import 'components/collapsable-nav';
 @import 'components/color-picker';
 @import 'components/color-picker';
+@import 'components/config-manager';
 @import 'components/footer';
 @import 'components/footer';
 @import 'components/form';
 @import 'components/form';
 @import 'components/grid';
 @import 'components/grid';
@@ -43,6 +44,7 @@ $primary: 'indigo';
 @import 'layout/_loader';
 @import 'layout/_loader';
 @import 'layout/_rtl';
 @import 'layout/_rtl';
 
 
-@import 'pages/_welcome';
+@import 'pages/login';
+@import 'pages/welcome';
 
 
 @import 'base/print';
 @import 'base/print';

+ 6 - 1
client/scss/base/base.scss

@@ -11,10 +11,15 @@ html {
   display: none;
   display: none;
 }
 }
 
 
-#root {
+#app {
   padding-bottom: 67px;
   padding-bottom: 67px;
   position: relative;
   position: relative;
   min-height: 100%;
   min-height: 100%;
+
+  &.is-fullscreen {
+    width: 100vw;
+    height: 100vh;
+  }
 }
 }
 
 
 body {
 body {

+ 18 - 4
client/scss/components/button.scss

@@ -4,7 +4,7 @@
 	border: 1px solid mc('orange','700');
 	border: 1px solid mc('orange','700');
 	border-radius: 3px;
 	border-radius: 3px;
 	display: inline-flex;
 	display: inline-flex;
-	height: 30px;
+	height: 40px;
 	align-items: center;
 	align-items: center;
 	padding: 0 15px;
 	padding: 0 15px;
 	font-size: 13px;
 	font-size: 13px;
@@ -61,7 +61,11 @@
 				background-color: mc($color,'800');
 				background-color: mc($color,'800');
 				color: #FFF;
 				color: #FFF;
 				animation: none;
 				animation: none;
-			}
+      }
+
+      &:focus {
+        box-shadow: inset 0 0 0 3px rgba(255,255,255, .4);
+      }
 		}
 		}
 
 
 	}
 	}
@@ -74,7 +78,13 @@
 
 
 	&.is-featured {
 	&.is-featured {
 		animation: btnInvertedPulse .6s ease alternate infinite;
 		animation: btnInvertedPulse .6s ease alternate infinite;
-	}
+  }
+
+  &.is-fullwidth {
+    width: 100%;
+    text-align: center;
+    justify-content: center;
+  }
 
 
 	&.is-disabled, &:disabled {
 	&.is-disabled, &:disabled {
 		background-color: mc('grey', '300');
 		background-color: mc('grey', '300');
@@ -87,7 +97,11 @@
       background-color: mc('grey', '300') !important;
       background-color: mc('grey', '300') !important;
       color: mc('grey', '500') !important;
       color: mc('grey', '500') !important;
     }
     }
-	}
+  }
+
+  &.is-small {
+    height: 30px;
+  }
 
 
 }
 }
 
 

+ 61 - 0
client/scss/components/config-manager.scss

@@ -0,0 +1,61 @@
+.config-manager {
+  .welcome {
+    text-align: center;
+    padding: 50px 0 15px 0;
+    color: mc('grey', '700');
+
+    h2 {
+      margin: 0;
+    }
+
+  }
+
+  i.icon-loader {
+    display: inline-block;
+    color: mc('indigo', '500')
+  }
+  i.icon-check {
+    color: mc('green', '500')
+  }
+  i.icon-square-cross {
+    color: mc('red', '500')
+  }
+  i.icon-warning-outline {
+    color: mc('orange', '500')
+  }
+
+  .tst-welcome-leave-active, .tst-welcome-enter-active {
+    transition: all .5s;
+    overflow-y: hidden;
+  }
+  .tst-welcome-leave, .tst-welcome-enter-to {
+    opacity: 1;
+    max-height: 200px;
+  }
+  .tst-welcome-leave-to, .tst-welcome-enter {
+    opacity: 0;
+    max-height: 0;
+    padding-top: 0;
+  }
+
+  .progress-bar {
+    width: 150px;
+    height: 10px;
+    background-color: mc('indigo', '50');
+    border:1px solid mc('indigo', '100');
+    border-radius: 3px;
+    position: absolute;
+    left: 15px;
+    top: 21px;
+    padding: 1px;
+
+    > div {
+      width: 5px;
+      height: 6px;
+      background-color: mc('indigo', '200');
+      border-radius: 2px;
+      transition: all 1s ease;
+    }
+
+  }
+}

+ 0 - 13
client/scss/login.scss

@@ -1,13 +0,0 @@
-@charset "utf-8";
-
-$primary: 'indigo';
-
-@import "base/variables";
-@import "base/colors";
-@import "base/reset";
-@import "base/mixins";
-@import "base/fonts";
-@import "base/base";
-
-@import "libs/animate";
-@import 'pages/login';

+ 263 - 305
client/scss/pages/_login.scss

@@ -1,306 +1,264 @@
-
-body {
-	padding: 0;
-	margin: 0;
-	font-family: $core-font-standard;
-	font-size: 14px;
-}
-
-a {
-	color: #FFF;
-	transition: color 0.4s ease;
-	text-decoration: none;
-
-	&:hover {
-		color: mc('orange','600');
-		text-decoration: underline;
-	}
-
-}
-
-#bg {
-	position: fixed;
-	top: 0;
-	left: 0;
-	width: 100%;
-	height: 100%;
-	z-index: 1;
-	background-color: #000;
-
-	> div {
-		background-size: cover;
-		background-position: center center;
-		width: 100%;
-		height: 100%;
-		position: absolute;
-		top: 0;
-		left: 0;
-		opacity: 0;
-		visibility: hidden;
-		transition: opacity 3s ease, visibility 3s;
-		animation: bg 30s linear infinite;
-
-		&:nth-child(1) {
-			animation-delay: 10s;
-		}
-
-		&:nth-child(2) {
-			animation-delay: 20s;
-		}
-
-	}
-
-}
-
-#root {
-	position: fixed;
-	top: 15vh;
-	left: 10vw;
-	z-index: 2;
-	color: #FFF;
-	display: flex;
-	flex-direction: column;
-
-	h1 {
-		font-size: 4rem;
-		font-weight: bold;
-		color: #FFF;
-		padding: 0;
-		margin: 0;
-		animation: headerIntro 3s ease;
-	}
-
-	h2 {
-		font-size: 1.5rem;
-		font-weight: normal;
-		color: rgba(255,255,255,0.7);
-		padding: 0;
-		margin: 0 0 25px 0;
-		animation: headerIntro 3s ease;
-	}
-
-	h3 {
-		font-size: 1.25rem;
-		font-weight: normal;
-		color: #FB8C00;
-		padding: 0;
-		margin: 0;
-		animation: shake 1s ease;
-
-		> .fa {
-			margin-right: 7px;
-		}
-
-	}
-
-	h4 {
-		font-size: 0.8rem;
-		font-weight: normal;
-		color: rgba(255,255,255,0.7);
-		padding: 0;
-		margin: 0 0 15px 0;
-		animation: fadeIn 3s ease;
-	}
-
-	form {
-		display: flex;
-		flex-direction: column;
-	}
-
-	input[type=text], input[type=password] {
-		width: 350px;
-		max-width: 80vw;
-		border: 1px solid rgba(255,255,255,0.3);
-		border-radius: 3px;
-		background-color: rgba(0,0,0,0.2);
-		padding: 0 15px;
-		height: 40px;
-		margin: 0 0 10px 0;
-		color: #FFF;
-		font-weight: bold;
-		font-size: 14px;
-		transition: all 0.4s ease;
-
-		&:focus {
-			outline: none;
-			border-color: mc('orange','600');
-		}
-
-	}
-
-	button {
-		background-color: mc('orange','600');
-		color: #FFF;
-		border: 1px solid lighten(mc('orange','600'), 10%);
-		border-radius: 3px;
-		height: 40px;
-		width: 125px;
-		padding: 0;
-		font-weight: bold;
-		margin: 15px 0 0 0;
-		transition: all 0.4s ease;
-		cursor: pointer;
-
-		span {
-			font-weight: bold;
-		}
-
-		&:focus {
-			outline: none;
-			border-color: #FFF;
-		}
-
-		&:hover {
-			background-color: darken(mc('orange','600'), 10%);
-		}
-
-	}
-
-	#social {
-		margin-top: 25px;
-
-		> span {
-			display: block;
-			font-weight: bold;
-			color: rgba(255,255,255,0.7);
-		}
-
-		button {
-			margin-right: 5px;
-			width: auto;
-			padding: 0 15px;
-
-			> i {
-				margin-right: 10px;
-				font-size: 16px;
-			}
-
-			&.ms {
-				background-color: mc('blue','600');
-				border-color: lighten(mc('blue','600'), 10%);
-
-				&:focus {
-					border-color: #FFF;
-				}
-
-				&:hover {
-					background-color: darken(mc('blue','600'), 10%);
-				}
-
-			}
-
-			&.google {
-				background-color: mc('light-blue','600');
-				border-color: lighten(mc('light-blue','600'), 10%);
-
-				&:focus {
-					border-color: #FFF;
-				}
-
-				&:hover {
-					background-color: darken(mc('light-blue','600'), 10%);
-				}
-
-			}
-
-			&.facebook {
-				background-color: mc('indigo','600');
-				border-color: lighten(mc('indigo','600'), 10%);
-
-				&:focus {
-					border-color: #FFF;
-				}
-
-				&:hover {
-					background-color: darken(mc('indigo','600'), 10%);
-				}
-
-			}
-
-      &.github {
-				background-color: mc('blue-grey','700');
-				border-color: lighten(mc('blue-grey','700'), 10%);
-
-				&:focus {
-					border-color: #FFF;
-				}
-
-				&:hover {
-					background-color: darken(mc('blue-grey','700'), 10%);
-				}
-			}
-
-      &.slack {
-				background-color: mc('purple','700');
-				border-color: lighten(mc('purple','700'), 10%);
-
-				&:focus {
-					border-color: #FFF;
-				}
-
-				&:hover {
-					background-color: darken(mc('purple','700'), 10%);
-				}
-			}
-
-		}
-
-	}
-
-}
-
-#copyright {
-	display: flex;
-	align-items: center;
-	justify-content: flex-start;
-	position: absolute;
-	left: 10vw;
-	bottom: 10vh;
-	z-index: 2;
-	color: rgba(255,255,255,0.5);
-	font-weight: bold;
-
-	.icon {
-		font-size: 1.2rem;
-		margin: 0 8px;
-	}
-
-	a {
-		opacity: 0.75;
-	}
-
-}
-
-@include keyframes(bg) {
-	0% {
-		@include prefix(transform, scale(1,1));
-		visibility: visible;
-		opacity: 0;
-	},
-	5% {
-		opacity: 0.5;
-	},
-	33% {
-		opacity: 0.5;
-	},
-	38% {
-		@include prefix(transform, scale(1.2, 1.2));
-		opacity: 0;
-	},
-	39% {
-		visibility: hidden;
-	}
-	100% {
-		visibility: hidden;
-		opacity: 0;
-	}
-}
-
-@include keyframes(headerIntro) {
-	0% {
-		opacity: 0;
-	}
-	100% {
-		opacity: 1;
-	}
+.login {
+  background-image: linear-gradient(to right, mc('blue', '400'), mc('blue', '600'));
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &.is-error {
+    background-image: linear-gradient(to right, mc('red', '400'), mc('red', '600'));
+  }
+
+  &::before {
+    content: '';
+    position: absolute;
+    background-image: url('../svg/login-bg.svg');
+    background-position: center bottom;
+    background-size: cover;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+  }
+
+  &-container {
+    position: relative;
+    display: flex;
+    width: 400px;
+    align-items: stretch;
+    box-shadow: 0 14px 28px rgba(0,0,0,0.2);
+    border-radius: 6px;
+
+    &.is-expanded {
+      width: 650px;
+
+      .login-frame {
+        border-radius: 0 6px 6px 0;
+        border-left: none;
+      }
+    }
+
+    @include until($tablet) {
+      width: 350px;
+
+      &.is-expanded {
+        width: 400px;
+      }
+    }
+  }
+
+  &-error {
+    position: absolute;
+    bottom: 105%;
+    width: 100%;
+    min-height: 50px;
+    background-image: radial-gradient(ellipse at top left, rgba(255,255,255,.9) 0%,rgba(255,255,255,.8) 100%);
+    box-shadow: 0 5px 10px rgba(0,0,0,0.2);
+    border-radius: 6px;
+    color: mc('red', '800');
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 1rem;
+
+    strong {
+      font-weight: 600;
+      text-transform: uppercase;
+      display: block;
+      padding: 0 1rem 0 0;
+      border-right: 1px solid mc('red', '200');
+    }
+    span {
+      padding: 0 0 0 1rem;
+      display: block;
+    }
+  }
+
+  &-providers {
+    display: flex;
+    flex-direction: column;
+    width: 250px;
+
+    border-right: none;
+    border-radius: 6px 0 0 6px;
+    z-index: 1;
+    overflow: hidden;
+
+    @include until($tablet) {
+      width: 50px;
+    }
+
+    button {
+      flex: 1 1;
+      padding: 5px 15px;
+      border: none;
+      color: #FFF;
+      background: linear-gradient(to right, rgba(mc('light-blue', '800'), .7), rgba(mc('light-blue', '800'), 1));
+      border-top: 1px solid rgba(mc('light-blue', '900'), .5);
+      font-family: $core-font-standard;
+      font-weight: 600;
+      text-align: left;
+      min-height: 40px;
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+      transition: all .4s ease;
+
+      &:focus {
+        outline: none;
+      }
+
+      @include until($tablet) {
+        justify-content: center;
+      }
+
+      &:hover {
+        background-color: mc('light-blue', '900');
+      }
+
+      &:first-child {
+        border-top: none;
+
+        &.is-active {
+          border-top: 1px solid rgba(255,255,255, .5);
+        }
+      }
+
+      &.is-active {
+        background-image: linear-gradient(to right, rgba(255,255,255,1) 0%,rgba(255,255,255,.77) 100%);
+        color: mc('light-blue', '700');
+        cursor: default;
+
+        &:hover {
+          background-color: transparent;
+        }
+
+        svg path {
+          fill: mc('light-blue', '800');
+        }
+      }
+
+      i {
+        margin-right: 10px;
+        font-size: 16px;
+
+        @include until($tablet) {
+          margin-right: 0;
+          font-size: 20px;
+        }
+      }
+
+      svg {
+        margin-right: 10px;
+        width: auto;
+        height: 20px;
+        max-width: 18px;
+        max-height: 20px;
+
+        path {
+          fill: #FFF;
+        }
+
+        @include until($tablet) {
+          margin-right: 0;
+          font-size: 20px;
+        }
+      }
+
+      span {
+        font-weight: 600;
+
+        @include until($tablet) {
+          display: none;
+        }
+      }
+    }
+  }
+
+  &-frame {
+    background-image: radial-gradient(circle at top center, rgba(255,255,255,1) 5%,rgba(255,255,255,.6) 100%);
+    border: 1px solid rgba(255,255,255, .5);
+    border-radius: 6px;
+    width: 400px;
+    padding: 1rem;
+    color: mc('grey', '700');
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+    text-align: center;
+
+    @include until($tablet) {
+      width: 350px;
+    }
+
+    h1 {
+      font-size: 2rem;
+      font-weight: 600;
+      color: mc('light-blue', '700');
+      text-shadow: 1px 1px 0 #FFF;
+      padding: 0;
+      margin: 0;
+    }
+
+    h2 {
+      font-size: 1.5rem;
+      font-weight: 300;
+      color: mc('grey', '700');
+      text-shadow: 1px 1px 0 #FFF;
+      padding: 0;
+      margin: 0 0 25px 0;
+    }
+
+    form {
+      display: flex;
+      flex-direction: column;
+    }
+
+    input[type=text], input[type=password] {
+      width: 100%;
+      border: 1px solid #FFF;
+      border-radius: 3px;
+      background-color: rgba(255,255,255,.9);
+      box-shadow: inset 0 0 0 3px rgba(255,255,255, .25);
+      padding: 0 15px;
+      height: 40px;
+      margin: 0 0 10px 0;
+      color: mc('grey', '700');
+      font-weight: 600;
+      font-size: .8rem;
+      transition: all 0.4s ease;
+      text-align: center;
+
+      &:focus {
+        outline: none;
+        border-color: mc('light-blue','500');
+        background-color: rgba(255,255,255,1);
+        box-shadow: inset 0 0 0 3px rgba(mc('light-blue','500'), .25);
+        color: mc('light-blue', '800');
+      }
+
+    }
+
+  }
+
+  &-copyright {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: absolute;
+    left: 0;
+    bottom: 10vh;
+    width: 100%;
+    z-index: 2;
+    color: mc('grey', '500');
+    font-weight: 400;
+
+    a {
+      font-weight: 600;
+      color: mc('light-blue', '500');
+      margin-left: .25rem;
+    }
+
+  }
 }
 }

+ 22 - 138
config.sample.yml

@@ -5,23 +5,9 @@
 # https://docs.requarks.io/wiki/install
 # https://docs.requarks.io/wiki/install
 
 
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
-# Title of this site
+# Port the main server should listen to
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
 
 
-title: Wiki
-
-# ---------------------------------------------------------------------
-# Full public path to the site, without the trailing slash
-# ---------------------------------------------------------------------
-# INCLUDE CLIENT PORT IF NOT 80/443!
-
-host: http://localhost
-
-# ---------------------------------------------------------------------
-# Port the main server should listen to (80 by default)
-# ---------------------------------------------------------------------
-# To use process.env.PORT, comment the line below:
-
 port: 80
 port: 80
 
 
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
@@ -33,140 +19,38 @@ paths:
   data: ./data
   data: ./data
 
 
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
-# Upload Limits
-# ---------------------------------------------------------------------
-# In megabytes (MB)
-
-uploads:
-  maxImageFileSize: 3
-  maxOtherFileSize: 100
-
-# ---------------------------------------------------------------------
-# Site Language
-# ---------------------------------------------------------------------
-# Possible values: en, de, es, fa, fr, ja, ko, nl, pt, ru, sr, tr or zh
-
-lang: en
-
-# Enable for right to left languages (e.g. arabic):
-langRtl: false
-
-# ---------------------------------------------------------------------
-# Site Authentication
-# ---------------------------------------------------------------------
-
-public: false
-
-auth:
-  defaultReadAccess: false
-  local:
-    enabled: true
-  google:
-    enabled: true
-    clientId: GOOGLE_CLIENT_ID
-    clientSecret: GOOGLE_CLIENT_SECRET
-  microsoft:
-    enabled: true
-    clientId: MS_APP_ID
-    clientSecret: MS_APP_SECRET
-  facebook:
-    enabled: false
-    clientId: FACEBOOK_APP_ID
-    clientSecret: FACEBOOK_APP_SECRET
-  github:
-    enabled: false
-    clientId: GITHUB_CLIENT_ID
-    clientSecret: GITHUB_CLIENT_SECRET
-  slack:
-    enabled: false
-    clientId: 'SLACK_CLIENT_ID'
-    clientSecret: 'SLACK_CLIENT_SECRET'
-  ldap:
-    enabled: false
-    url: ldap://serverhost:389
-    bindDn: cn='root'
-    bindCredentials: BIND_PASSWORD
-    searchBase: o=users,o=example.com
-    searchFilter: (uid={{username}})
-    tlsEnabled: false
-    tlsCertPath: C:\example\root_ca_cert.crt
-  azure:
-    enabled: false
-    clientId: APP_ID
-    clientSecret: APP_SECRET_KEY
-    resource: '00000002-0000-0000-c000-000000000000'
-    tenant: 'YOUR_TENANT.onmicrosoft.com'
-
-# ---------------------------------------------------------------------
-# Secret key to use when encrypting sessions
-# ---------------------------------------------------------------------
-# Use a long and unique random string (256-bit keys are perfect!)
-
-sessionSecret: 1234567890abcdefghijklmnopqrstuvxyz
-
-# ---------------------------------------------------------------------
-# Database Connection String
-# ---------------------------------------------------------------------
-# You can also use an ENV variable by using $ENV_VAR_NAME as the value
-
-db: mongodb://localhost:27017/wiki
-
-# ---------------------------------------------------------------------
-# Git Connection Info
+# Database
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
 
 
-git:
-  url: https://github.com/Organization/Repo
-  branch: master
-  auth:
-
-    # Type: basic or ssh
-    type: ssh
-
-    # Only for Basic authentication:
-    username: marty
-    password: MartyMcFly88
-
-    # Only for SSH authentication:
-    privateKey: /etc/wiki/keys/git.pem
-
-    sslVerify: true
-
-  # Default email to use as commit author
-  serverEmail: marty@example.com
-
-  # Whether to use user email as author in commits
-  showUserEmail: true
+db:
+  host: localhost
+  port: 5432
+  user: wikijs
+  pass: wikijsrocks
+  db: wiki
 
 
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
-# Features
+# Redis
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
-# You can enable / disable specific features below
 
 
-features:
-  linebreaks: true
-  mathjax: true
+redis:
+  host: localhost
+  port: 6379
+  db: 0
+  password: null
 
 
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
-# External Logging
+# Background Workers
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
+# Leave 0 for auto based on CPU cores
 
 
-externalLogging:
-  bugsnag: false
-  loggly: false
-  papertrail: false
-  rollbar: false
-  sentry: false
+workers: 0
 
 
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
-# Color Theme
+# High Availability
 # ---------------------------------------------------------------------
 # ---------------------------------------------------------------------
+# Read the docs BEFORE changing these settings!
 
 
-theme:
-  primary: indigo
-  alt: blue-grey
-  viewSource: all # all | write | false
-  footer: blue-grey
-  code:
-    dark: true
-    colorize: true
+ha:
+  nodeuid: primary
+  readonly: false

+ 0 - 14
npm/README.md

@@ -1,14 +0,0 @@
-![Wiki.js](https://raw.githubusercontent.com/Requarks/wiki-site/1.0/assets/images/logo.png)
-
-# Wiki.js
-
-[![npm](https://img.shields.io/npm/v/wiki.js.svg?style=flat-square)](https://github.com/Requarks)
-[![Release](https://img.shields.io/github/release/Requarks/wiki.svg?style=flat-square&maxAge=3600)](https://github.com/Requarks/wiki/releases)
-[![License](https://img.shields.io/badge/license-AGPLv3-blue.svg?style=flat-square)](https://github.com/requarks/wiki/blob/master/LICENSE)
-
-This npm package is an installer for Wiki.js.  
-For information about Wiki.js, including detailed installation steps, read the following links:
-
-- [Official Website](https://wiki.js.org/)
-- [Installation Guide](https://wiki.js.org/get-started.html)
-- [GitHub Repository](https://github.com/Requarks/wiki)

+ 0 - 25
npm/install.js

@@ -1,25 +0,0 @@
-'use strict'
-
-// =====================================================
-// Wiki.js
-// Installation Script
-// =====================================================
-
-const path = require('path')
-const spawn = require('child_process').spawn
-const installDir = path.resolve(__dirname, '../..')
-const cmd = (process.platform !== 'win32')
-  ? 'curl -s -S -o- https://wiki.js.org/install.sh | bash'
-  : `PowerShell.exe -NoProfile -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://wiki.js.org/install.ps1'))"`
-
-console.info(`Executing installation script for ${process.platform} platform...`)
-
-let inst = spawn(cmd, [], {
-  cwd: installDir,
-  env: process.env,
-  shell: true,
-  stdio: 'inherit',
-  detached: true
-})
-
-inst.unref()

+ 0 - 32
npm/package.json

@@ -1,32 +0,0 @@
-{
-  "name": "wiki.js",
-  "version": "1.0.5-rev.1",
-  "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
-  "main": "install.js",
-  "scripts": {
-    "test": "exit 1",
-    "postinstall": "node install.js"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/Requarks/wiki.git"
-  },
-  "keywords": [
-    "wiki",
-    "wikis",
-    "wikijs",
-    "wiki.js",
-    "wiki-js",
-    "docs",
-    "documentation",
-    "markdown",
-    "guides"
-  ],
-  "author": "Nicolas Giard",
-  "license": "AGPL-3.0",
-  "bugs": {
-    "url": "https://github.com/Requarks/wiki/issues"
-  },
-  "homepage": "https://github.com/Requarks/wiki#readme",
-  "dependencies": {}
-}

+ 132 - 125
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "wiki",
   "name": "wiki",
-  "version": "1.0.11",
+  "version": "2.0.0",
   "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
   "description": "A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown",
   "main": "wiki.js",
   "main": "wiki.js",
   "scripts": {
   "scripts": {
@@ -9,7 +9,6 @@
     "restart": "node wiki restart",
     "restart": "node wiki restart",
     "build": "node tools/fuse",
     "build": "node tools/fuse",
     "dev": "node tools/fuse -d",
     "dev": "node tools/fuse -d",
-    "dev-configure": "node tools/fuse -c",
     "test": "jest",
     "test": "jest",
     "postinstall": "opencollective postinstall"
     "postinstall": "opencollective postinstall"
   },
   },
@@ -38,135 +37,143 @@
     "node": ">=6.11.1"
     "node": ">=6.11.1"
   },
   },
   "dependencies": {
   "dependencies": {
-    "auto-load": "~3.0.0",
-    "axios": "~0.16.2",
-    "bcryptjs-then": "~1.0.1",
-    "bluebird": "~3.5.0",
-    "body-parser": "~1.17.2",
-    "bunyan": "~1.8.12",
-    "cheerio": "~1.0.0-rc.2",
-    "child-process-promise": "~2.2.1",
-    "chokidar": "~1.7.0",
-    "compression": "~1.7.0",
-    "connect-flash": "~0.1.1",
-    "connect-mongo": "~1.3.2",
-    "cookie-parser": "~1.4.3",
-    "cron": "~1.2.1",
-    "diff2html": "~2.3.0",
-    "execa": "~0.8.0",
-    "express": "~4.15.4",
+    "apollo-server-express": "1.1.3",
+    "auto-load": "3.0.0",
+    "axios": "0.16.2",
+    "bcryptjs-then": "1.0.1",
+    "bluebird": "3.5.1",
+    "body-parser": "1.18.2",
+    "bull": "3.3.0",
+    "bunyan": "1.8.12",
+    "cheerio": "1.0.0-rc.2",
+    "child-process-promise": "2.2.1",
+    "chokidar": "1.7.0",
+    "compression": "1.7.1",
+    "connect-flash": "0.1.1",
+    "connect-redis": "3.3.2",
+    "cookie-parser": "1.4.3",
+    "diff2html": "2.3.1",
+    "dotize": "^0.2.0",
+    "execa": "0.8.0",
+    "express": "4.16.1",
     "express-brute": "1.0.1",
     "express-brute": "1.0.1",
-    "express-brute-mongoose": "~0.0.9",
-    "express-session": "~1.15.5",
-    "file-type": "~6.1.0",
-    "filesize.js": "~1.0.2",
-    "follow-redirects": "~1.2.4",
-    "fs-extra": "~4.0.1",
-    "git-wrapper2-promise": "~0.2.9",
-    "highlight.js": "~9.12.0",
-    "i18next": "~9.0.0",
-    "i18next-express-middleware": "~1.0.5",
-    "i18next-node-fs-backend": "~1.0.0",
-    "image-size": "~0.6.0",
-    "jimp": "~0.2.28",
-    "js-yaml": "~3.9.1",
-    "jsonwebtoken": "~7.4.3",
-    "klaw": "~2.1.0",
-    "levelup": "~1.3.9",
-    "lodash": "~4.17.4",
-    "markdown-it": "~8.4.0",
-    "markdown-it-abbr": "~1.0.4",
-    "markdown-it-anchor": "~4.0.0",
-    "markdown-it-attrs": "~1.1.0",
-    "markdown-it-emoji": "~1.4.0",
-    "markdown-it-expand-tabs": "~1.0.12",
+    "express-brute-redis": "0.0.1",
+    "express-session": "1.15.6",
+    "file-type": "6.2.0",
+    "filesize.js": "1.0.2",
+    "follow-redirects": "1.2.5",
+    "fs-extra": "4.0.2",
+    "git-wrapper2-promise": "0.2.9",
+    "graphql": "0.10.5",
+    "graphql-tools": "2.2.1",
+    "highlight.js": "9.12.0",
+    "i18next": "9.1.0",
+    "i18next-express-middleware": "1.0.7",
+    "i18next-localstorage-cache": "1.1.1",
+    "i18next-node-fs-backend": "1.0.0",
+    "image-size": "0.6.1",
+    "ioredis": "3.1.4",
+    "jimp": "0.2.28",
+    "js-yaml": "3.10.0",
+    "jsonwebtoken": "8.0.1",
+    "klaw": "2.1.0",
+    "lodash": "4.17.4",
+    "markdown-it": "8.4.0",
+    "markdown-it-abbr": "1.0.4",
+    "markdown-it-anchor": "4.0.0",
+    "markdown-it-attrs": "1.2.0",
+    "markdown-it-emoji": "1.4.0",
+    "markdown-it-expand-tabs": "1.0.12",
     "markdown-it-external-links": "0.0.6",
     "markdown-it-external-links": "0.0.6",
-    "markdown-it-footnote": "~3.0.1",
-    "markdown-it-mathjax": "~2.0.0",
-    "markdown-it-task-lists": "~2.0.1",
-    "mathjax-node": "~1.2.0",
-    "memdown": "~1.2.4",
-    "mime-types": "~2.1.16",
-    "moment": "~2.18.1",
-    "moment-timezone": "~0.5.13",
-    "mongodb": "~2.2.31",
-    "mongoose": "~4.11.9",
-    "multer": "~1.3.0",
-    "node-2fa": "~1.1.2",
-    "node-graceful": "~0.2.3",
-    "opencollective": "~1.0.3",
-    "ora": "~1.3.0",
-    "passport": "~0.4.0",
+    "markdown-it-footnote": "3.0.1",
+    "markdown-it-mathjax": "2.0.0",
+    "markdown-it-task-lists": "2.0.1",
+    "mathjax-node": "1.2.1",
+    "mime-types": "2.1.17",
+    "moment": "2.18.1",
+    "moment-timezone": "0.5.13",
+    "multer": "1.3.0",
+    "node-2fa": "1.1.2",
+    "node-graceful": "0.2.3",
+    "ora": "1.3.0",
+    "passport": "0.4.0",
     "passport-azure-ad-oauth2": "0.0.4",
     "passport-azure-ad-oauth2": "0.0.4",
-    "passport-facebook": "~2.1.1",
-    "passport-github2": "~0.1.10",
-    "passport-google-oauth20": "~1.0.0",
-    "passport-ldapauth": "~2.0.0",
-    "passport-local": "~1.0.0",
+    "passport-facebook": "2.1.1",
+    "passport-github2": "0.1.11",
+    "passport-google-oauth20": "1.0.0",
+    "passport-ldapauth": "2.0.0",
+    "passport-local": "1.0.0",
     "passport-slack": "0.0.7",
     "passport-slack": "0.0.7",
-    "passport-windowslive": "~1.0.2",
-    "passport.socketio": "~3.7.0",
-    "pm2": "~2.6.1",
-    "pug": "~2.0.0-rc.3",
-    "read-chunk": "~2.1.0",
-    "remove-markdown": "~0.2.2",
-    "request": "~2.81.0",
-    "search-index-adder": "~0.3.9",
-    "search-index-searcher": "~0.2.10",
-    "semver": "~5.4.1",
-    "serve-favicon": "~2.4.3",
-    "simplemde": "~1.11.2",
-    "socket.io": "~2.0.2",
-    "stopword": "~0.1.6",
-    "stream-to-promise": "~2.2.0",
-    "tar": "~4.0.1",
-    "through2": "~2.0.3",
-    "validator": "~8.1.0",
-    "validator-as-promised": "~1.0.2",
-    "winston": "~2.3.1",
-    "yargs": "~8.0.1"
+    "passport-windowslive": "1.0.2",
+    "pg": "6.4.2",
+    "pg-hstore": "2.3.2",
+    "pg-promise": "6.10.3",
+    "pm2": "2.7.1",
+    "pug": "2.0.0-rc.4",
+    "qr-image": "3.2.0",
+    "read-chunk": "2.1.0",
+    "remove-markdown": "0.2.2",
+    "request": "2.83.0",
+    "semver": "5.4.1",
+    "sequelize": "4.13.5",
+    "serve-favicon": "2.4.5",
+    "simplemde": "1.11.2",
+    "stream-to-promise": "2.2.0",
+    "tar": "4.0.1",
+    "through2": "2.0.3",
+    "validator": "9.0.0",
+    "validator-as-promised": "1.0.2",
+    "winston": "2.4.0",
+    "yargs": "9.0.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "@glimpse/glimpse": "~0.22.15",
-    "@panter/vue-i18next": "~0.5.0",
-    "babel-cli": "~6.26.0",
-    "babel-jest": "~20.0.3",
-    "babel-plugin-transform-object-assign": "~6.22.0",
-    "babel-preset-es2015": "~6.24.1",
-    "brace": "~0.10.0",
-    "colors": "~1.1.2",
-    "consolidate": "~0.14.5",
-    "eslint": "~4.5.0",
-    "eslint-config-standard": "~10.2.1",
-    "eslint-plugin-import": "~2.7.0",
-    "eslint-plugin-node": "~5.1.0",
-    "eslint-plugin-promise": "~3.5.0",
-    "eslint-plugin-standard": "~3.0.1",
-    "fuse-box": "~2.2.2",
-    "i18next-xhr-backend": "~1.4.2",
-    "jest": "~20.0.4",
-    "jest-junit": "~3.1.0",
-    "jquery": "~3.2.1",
-    "jquery-contextmenu": "~2.5.0",
-    "jquery-simple-upload": "~1.0.0",
-    "jquery-smooth-scroll": "~2.2.0",
-    "jquery-sticky": "~1.0.4",
-    "lodash-cli": "~4.17.4",
-    "lodash-es": "~4.17.4",
-    "node-sass": "~4.5.3",
-    "nodemon": "~1.11.0",
-    "pug-lint": "~2.4.0",
-    "twemoji-awesome": "~1.0.6",
-    "typescript": "~2.5.2",
-    "uglify-es": "~3.0.28",
-    "vee-validate": "~2.0.0-rc.14",
-    "vue": "~2.4.2",
-    "vue-clipboards": "~1.1.0",
-    "vue-lodash": "~1.0.3",
-    "vue-resource": "~1.3.4",
-    "vue-template-compiler": "~2.4.2",
-    "vue-template-es2015-compiler": "~1.5.3",
-    "vuex": "~2.4.0"
+    "@glimpse/glimpse": "0.22.15",
+    "@panter/vue-i18next": "0.6.1",
+    "apollo-client": "^1.9.3",
+    "autoprefixer": "7.1.5",
+    "babel-cli": "6.26.0",
+    "babel-core": "6.26.0",
+    "babel-jest": "21.2.0",
+    "babel-preset-env": "1.6.0",
+    "babel-preset-es2015": "6.24.1",
+    "babel-preset-stage-2": "6.24.1",
+    "brace": "0.10.0",
+    "colors": "1.1.2",
+    "consolidate": "0.14.5",
+    "eslint": "4.8.0",
+    "eslint-config-requarks": "1.0.7",
+    "eslint-config-standard": "10.2.1",
+    "eslint-plugin-import": "2.7.0",
+    "eslint-plugin-node": "5.2.0",
+    "eslint-plugin-promise": "3.5.0",
+    "eslint-plugin-standard": "3.0.1",
+    "fuse-box": "2.3.3",
+    "graphql-tag": "^2.4.2",
+    "i18next-xhr-backend": "1.4.3",
+    "jest": "21.2.1",
+    "jquery": "3.2.1",
+    "jquery-contextmenu": "2.6.2",
+    "jquery-simple-upload": "1.0.0",
+    "js-cookie": "2.1.4",
+    "node-sass": "4.5.3",
+    "nodemon": "1.12.1",
+    "postcss-selector-parser": "2.2.3",
+    "pug-lint": "2.5.0",
+    "twemoji-awesome": "1.0.6",
+    "typescript": "2.5.3",
+    "uglify-es": "3.1.3",
+    "vee-validate": "2.0.0-rc.18",
+    "vue": "2.4.4",
+    "vue-clipboards": "1.1.0",
+    "vue-hot-reload-api": "2.1.1",
+    "vue-lodash": "1.0.4",
+    "vue-material": "^0.7.5",
+    "vue-resource": "1.3.4",
+    "vue-simple-breakpoints": "1.0.2",
+    "vue-template-compiler": "2.4.4",
+    "vue-template-es2015-compiler": "1.5.3",
+    "vuex": "2.4.1",
+    "vuex-persistedstate": "2.0.0"
   },
   },
   "jest": {
   "jest": {
     "testResultsProcessor": "./node_modules/jest-junit",
     "testResultsProcessor": "./node_modules/jest-junit",

+ 0 - 222
server/agent.js

@@ -1,222 +0,0 @@
-// ===========================================
-// Wiki.js - Background Agent
-// 1.0.0
-// Licensed under AGPLv3
-// ===========================================
-
-const path = require('path')
-const ROOTPATH = process.cwd()
-const SERVERPATH = path.join(ROOTPATH, 'server')
-
-global.ROOTPATH = ROOTPATH
-global.SERVERPATH = SERVERPATH
-const IS_DEBUG = process.env.NODE_ENV === 'development'
-
-let appconf = require('./libs/config')()
-global.appconfig = appconf.config
-global.appdata = appconf.data
-
-// ----------------------------------------
-// Load Winston
-// ----------------------------------------
-
-global.winston = require('./libs/logger')(IS_DEBUG, 'AGENT')
-
-// ----------------------------------------
-// Load global modules
-// ----------------------------------------
-
-global.winston.info('Background Agent is initializing...')
-
-global.db = require('./libs/db').init()
-global.upl = require('./libs/uploads-agent').init()
-global.git = require('./libs/git').init()
-global.entries = require('./libs/entries').init()
-global.lang = require('i18next')
-global.mark = require('./libs/markdown')
-
-// ----------------------------------------
-// Load modules
-// ----------------------------------------
-
-const moment = require('moment')
-const Promise = require('bluebird')
-const fs = Promise.promisifyAll(require('fs-extra'))
-const klaw = require('klaw')
-const Cron = require('cron').CronJob
-const i18nBackend = require('i18next-node-fs-backend')
-
-const entryHelper = require('./helpers/entry')
-
-// ----------------------------------------
-// Localization Engine
-// ----------------------------------------
-
-global.lang
-  .use(i18nBackend)
-  .init({
-    load: 'languageOnly',
-    ns: ['common', 'admin', 'auth', 'errors', 'git'],
-    defaultNS: 'common',
-    saveMissing: false,
-    preload: [appconfig.lang],
-    lng: appconfig.lang,
-    fallbackLng: 'en',
-    backend: {
-      loadPath: path.join(SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
-    }
-  })
-
-// ----------------------------------------
-// Start Cron
-// ----------------------------------------
-
-let job
-let jobIsBusy = false
-let jobUplWatchStarted = false
-
-global.db.onReady.then(() => {
-  return global.db.Entry.remove({})
-}).then(() => {
-  job = new Cron({
-    cronTime: '0 */5 * * * *',
-    onTick: () => {
-      // Make sure we don't start two concurrent jobs
-
-      if (jobIsBusy) {
-        global.winston.warn('Previous job has not completed gracefully or is still running! Skipping for now. (This is not normal, you should investigate)')
-        return
-      }
-      global.winston.info('Running all jobs...')
-      jobIsBusy = true
-
-      // Prepare async job collector
-
-      let jobs = []
-      let repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
-      let dataPath = path.resolve(ROOTPATH, appconfig.paths.data)
-      let uploadsTempPath = path.join(dataPath, 'temp-upload')
-
-      // ----------------------------------------
-      // REGULAR JOBS
-      // ----------------------------------------
-
-      //* ****************************************
-      // -> Sync with Git remote
-      //* ****************************************
-
-      jobs.push(global.git.resync().then(() => {
-        // -> Stream all documents
-
-        let cacheJobs = []
-        let jobCbStreamDocsResolve = null
-        let jobCbStreamDocs = new Promise((resolve, reject) => {
-          jobCbStreamDocsResolve = resolve
-        })
-
-        klaw(repoPath).on('data', function (item) {
-          if (path.extname(item.path) === '.md' && path.basename(item.path) !== 'README.md') {
-            let entryPath = entryHelper.parsePath(entryHelper.getEntryPathFromFullPath(item.path))
-            let cachePath = entryHelper.getCachePath(entryPath)
-
-            // -> Purge outdated cache
-
-            cacheJobs.push(
-              fs.statAsync(cachePath).then((st) => {
-                return moment(st.mtime).isBefore(item.stats.mtime) ? 'expired' : 'active'
-              }).catch((err) => {
-                return (err.code !== 'EEXIST') ? err : 'new'
-              }).then((fileStatus) => {
-                // -> Delete expired cache file
-
-                if (fileStatus === 'expired') {
-                  return fs.unlinkAsync(cachePath).return(fileStatus)
-                }
-
-                return fileStatus
-              }).then((fileStatus) => {
-                // -> Update cache and search index
-
-                if (fileStatus !== 'active') {
-                  return global.entries.updateCache(entryPath).then(entry => {
-                    process.send({
-                      action: 'searchAdd',
-                      content: entry
-                    })
-                    return true
-                  })
-                }
-
-                return true
-              })
-            )
-          }
-        }).on('end', () => {
-          jobCbStreamDocsResolve(Promise.all(cacheJobs))
-        })
-
-        return jobCbStreamDocs
-      }))
-
-      //* ****************************************
-      // -> Clear failed temporary upload files
-      //* ****************************************
-
-      jobs.push(
-        fs.readdirAsync(uploadsTempPath).then((ls) => {
-          let fifteenAgo = moment().subtract(15, 'minutes')
-
-          return Promise.map(ls, (f) => {
-            return fs.statAsync(path.join(uploadsTempPath, f)).then((s) => { return { filename: f, stat: s } })
-          }).filter((s) => { return s.stat.isFile() }).then((arrFiles) => {
-            return Promise.map(arrFiles, (f) => {
-              if (moment(f.stat.ctime).isBefore(fifteenAgo, 'minute')) {
-                return fs.unlinkAsync(path.join(uploadsTempPath, f.filename))
-              } else {
-                return true
-              }
-            })
-          })
-        })
-      )
-
-      // ----------------------------------------
-      // Run
-      // ----------------------------------------
-
-      Promise.all(jobs).then(() => {
-        global.winston.info('All jobs completed successfully! Going to sleep for now.')
-
-        if (!jobUplWatchStarted) {
-          jobUplWatchStarted = true
-          global.upl.initialScan().then(() => {
-            job.start()
-          })
-        }
-
-        return true
-      }).catch((err) => {
-        global.winston.error('One or more jobs have failed: ', err)
-      }).finally(() => {
-        jobIsBusy = false
-      })
-    },
-    start: false,
-    timeZone: 'UTC',
-    runOnInit: true
-  })
-})
-
-// ----------------------------------------
-// Shutdown gracefully
-// ----------------------------------------
-
-process.on('disconnect', () => {
-  global.winston.warn('Lost connection to main server. Exiting...')
-  job.stop()
-  process.exit()
-})
-
-process.on('exit', () => {
-  job.stop()
-})

+ 45 - 57
server/app/data.yml

@@ -5,67 +5,49 @@
 name: Wiki.js
 name: Wiki.js
 defaults:
 defaults:
   config:
   config:
-    title: Wiki
-    host: http://localhost
     port: 80
     port: 80
     paths:
     paths:
       repo: ./repo
       repo: ./repo
       data: ./data
       data: ./data
-    uploads:
-      maxImageFileSize: 3,
-      maxOtherFileSize: 100
-    lang: en
-    langRtl: false
-    public: false
-    auth:
-      defaultReadAccess: false
-      local:
-        enabled: true
-      microsoft:
-        enabled: false
-      google:
-        enabled: false
-      facebook:
-        enabled: false
-      github:
-        enabled: false
-      slack:
-        enabled: false
-      ldap:
-        enabled: false
-      azure:
-        enabled: false
-    db: mongodb://localhost/wiki
-    sessionSecret: null
-    admin: null
-    git:
-      url: null
-      branch: master
-      auth:
-        type: basic
-        username: null
-        password: null
-        privateKey: null
-        sslVerify: true
-      serverEmail: wiki@example.com
-      showUserEmail: true
-    features:
-      linebreaks: true
-      mathjax: true
-    externalLogging:
-      bugsnap: false
-      loggly: false
-      papertrail: false
-      rollbar: false
-      sentry: false
-    theme:
-      primary: indigo
-      alt: blue-grey
-      footer: blue-grey
-      viewSource: false
-      code:
-        dark: true
-        colorize: true
+    db:
+      host: localhost
+      port: 5432
+      user: wikijs
+      pass: wikijsrocks
+      db: wiki
+    redis:
+      host: localhost
+      port: 6379
+      db: 0
+      password: null
+    workers: 0
+    ha:
+      nodeuid: primary
+      readonly: false
+    site:
+      path: ''
+      lang: en
+    title: Wiki.js
+configNamespaces:
+  - auth
+  - features
+  - git
+  - logging
+  - site
+  - theme
+  - uploads
+queues:
+  - gitSync
+  - uplClearTemp
+authProviders:
+  - local
+  - microsoft
+  - google
+  - facebook
+  - github
+  - slack
+  - ldap
+  - azure
 colors:
 colors:
   - red
   - red
   - pink
   - pink
@@ -108,6 +90,12 @@ langs:
   -
   -
     id: ko
     id: ko
     name: Korean - 한국어
     name: Korean - 한국어
+  -
+    id: fa
+    name: Persian (Fārsi) - فارسی
+  -
+    id: pt
+    name: Portuguese - Português
   -
   -
     id: ru
     id: ru
     name: Russian - Русский
     name: Russian - Русский

+ 37 - 0
server/authentication/azure.js

@@ -0,0 +1,37 @@
+'use strict'
+
+/* global wiki */
+
+// ------------------------------------
+// Azure AD Account
+// ------------------------------------
+
+const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2').Strategy
+
+module.exports = {
+  key: 'azure',
+  title: 'Azure Active Directory',
+  useForm: false,
+  props: ['clientId', 'clientSecret', 'callbackURL', 'resource', 'tenant'],
+  init (passport, conf) {
+    const jwt = require('jsonwebtoken')
+    passport.use('azure_ad_oauth2',
+      new AzureAdOAuth2Strategy({
+        clientID: conf.clientId,
+        clientSecret: conf.clientSecret,
+        callbackURL: conf.callbackURL,
+        resource: conf.resource,
+        tenant: conf.tenant
+      }, (accessToken, refreshToken, params, profile, cb) => {
+        let waadProfile = jwt.decode(params.id_token)
+        waadProfile.id = waadProfile.oid
+        waadProfile.provider = 'azure'
+        wiki.db.User.processProfile(waadProfile).then((user) => {
+          return cb(null, user) || true
+        }).catch((err) => {
+          return cb(err, null) || true
+        })
+      }
+      ))
+  }
+}

+ 32 - 0
server/authentication/facebook.js

@@ -0,0 +1,32 @@
+'use strict'
+
+/* global wiki */
+
+// ------------------------------------
+// Facebook Account
+// ------------------------------------
+
+const FacebookStrategy = require('passport-facebook').Strategy
+
+module.exports = {
+  key: 'facebook',
+  title: 'Facebook',
+  useForm: false,
+  props: ['clientId', 'clientSecret', 'callbackURL'],
+  init (passport, conf) {
+    passport.use('facebook',
+      new FacebookStrategy({
+        clientID: conf.clientId,
+        clientSecret: conf.clientSecret,
+        callbackURL: conf.callbackURL,
+        profileFields: ['id', 'displayName', 'email']
+      }, function (accessToken, refreshToken, profile, cb) {
+        wiki.db.User.processProfile(profile).then((user) => {
+          return cb(null, user) || true
+        }).catch((err) => {
+          return cb(err, null) || true
+        })
+      }
+      ))
+  }
+}

+ 32 - 0
server/authentication/github.js

@@ -0,0 +1,32 @@
+'use strict'
+
+/* global wiki */
+
+// ------------------------------------
+// GitHub Account
+// ------------------------------------
+
+const GitHubStrategy = require('passport-github2').Strategy
+
+module.exports = {
+  key: 'github',
+  title: 'GitHub',
+  useForm: false,
+  props: ['clientId', 'clientSecret', 'callbackURL'],
+  init (passport, conf) {
+    passport.use('github',
+      new GitHubStrategy({
+        clientID: conf.clientId,
+        clientSecret: conf.clientSecret,
+        callbackURL: conf.callbackURL,
+        scope: ['user:email']
+      }, (accessToken, refreshToken, profile, cb) => {
+        wiki.db.User.processProfile(profile).then((user) => {
+          return cb(null, user) || true
+        }).catch((err) => {
+          return cb(err, null) || true
+        })
+      }
+      ))
+  }
+}

+ 31 - 0
server/authentication/google.js

@@ -0,0 +1,31 @@
+'use strict'
+
+/* global wiki */
+
+// ------------------------------------
+// Google ID Account
+// ------------------------------------
+
+const GoogleStrategy = require('passport-google-oauth20').Strategy
+
+module.exports = {
+  key: 'google',
+  title: 'Google ID',
+  useForm: false,
+  props: ['clientId', 'clientSecret', 'callbackURL'],
+  init (passport, conf) {
+    passport.use('google',
+      new GoogleStrategy({
+        clientID: conf.clientId,
+        clientSecret: conf.clientSecret,
+        callbackURL: conf.callbackURL
+      }, (accessToken, refreshToken, profile, cb) => {
+        wiki.db.User.processProfile(profile).then((user) => {
+          return cb(null, user) || true
+        }).catch((err) => {
+          return cb(err, null) || true
+        })
+      }
+      ))
+  }
+}

+ 46 - 0
server/authentication/ldap.js

@@ -0,0 +1,46 @@
+'use strict'
+
+/* global wiki */
+
+// ------------------------------------
+// LDAP Account
+// ------------------------------------
+
+const LdapStrategy = require('passport-ldapauth').Strategy
+const fs = require('fs')
+
+module.exports = {
+  key: 'ldap',
+  title: 'LDAP / Active Directory',
+  useForm: true,
+  props: ['url', 'bindDn', 'bindCredentials', 'searchBase', 'searchFilter', 'tlsEnabled', 'tlsCertPath'],
+  init (passport, conf) {
+    passport.use('ldapauth',
+      new LdapStrategy({
+        server: {
+          url: conf.url,
+          bindDn: conf.bindDn,
+          bindCredentials: conf.bindCredentials,
+          searchBase: conf.searchBase,
+          searchFilter: conf.searchFilter,
+          searchAttributes: ['displayName', 'name', 'cn', 'mail'],
+          tlsOptions: (conf.tlsEnabled) ? {
+            ca: [
+              fs.readFileSync(conf.tlsCertPath)
+            ]
+          } : {}
+        },
+        usernameField: 'email',
+        passReqToCallback: false
+      }, (profile, cb) => {
+        profile.provider = 'ldap'
+        profile.id = profile.dn
+        wiki.db.User.processProfile(profile).then((user) => {
+          return cb(null, user) || true
+        }).catch((err) => {
+          return cb(err, null) || true
+        })
+      }
+      ))
+  }
+}

+ 38 - 0
server/authentication/local.js

@@ -0,0 +1,38 @@
+'use strict'
+
+/* global wiki */
+
+// ------------------------------------
+// Local Account
+// ------------------------------------
+
+const LocalStrategy = require('passport-local').Strategy
+
+module.exports = {
+  key: 'local',
+  title: 'Local',
+  useForm: true,
+  props: [],
+  init (passport, conf) {
+    passport.use('local',
+      new LocalStrategy({
+        usernameField: 'email',
+        passwordField: 'password'
+      }, (uEmail, uPassword, done) => {
+        wiki.db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => {
+          if (user) {
+            return user.validatePassword(uPassword).then(() => {
+              return done(null, user) || true
+            }).catch((err) => {
+              return done(err, null)
+            })
+          } else {
+            return done(new Error('INVALID_LOGIN'), null)
+          }
+        }).catch((err) => {
+          done(err, null)
+        })
+      }
+      ))
+  }
+}

+ 31 - 0
server/authentication/microsoft.js

@@ -0,0 +1,31 @@
+'use strict'
+
+/* global wiki */
+
+// ------------------------------------
+// Microsoft Account
+// ------------------------------------
+
+const WindowsLiveStrategy = require('passport-windowslive').Strategy
+
+module.exports = {
+  key: 'microsoft',
+  title: 'Microsoft Account',
+  useForm: false,
+  props: ['clientId', 'clientSecret', 'callbackURL'],
+  init (passport, conf) {
+    passport.use('microsoft',
+      new WindowsLiveStrategy({
+        clientID: conf.clientId,
+        clientSecret: conf.clientSecret,
+        callbackURL: conf.callbackURL
+      }, function (accessToken, refreshToken, profile, cb) {
+        wiki.db.User.processProfile(profile).then((user) => {
+          return cb(null, user) || true
+        }).catch((err) => {
+          return cb(err, null) || true
+        })
+      }
+      ))
+  }
+}

+ 31 - 0
server/authentication/slack.js

@@ -0,0 +1,31 @@
+'use strict'
+
+/* global wiki */
+
+// ------------------------------------
+// Slack Account
+// ------------------------------------
+
+const SlackStrategy = require('passport-slack').Strategy
+
+module.exports = {
+  key: 'slack',
+  title: 'Slack',
+  useForm: false,
+  props: ['clientId', 'clientSecret', 'callbackURL'],
+  init (passport, conf) {
+    passport.use('slack',
+      new SlackStrategy({
+        clientID: conf.clientId,
+        clientSecret: conf.clientSecret,
+        callbackURL: conf.callbackURL
+      }, (accessToken, refreshToken, profile, cb) => {
+        wiki.db.User.processProfile(profile).then((user) => {
+          return cb(null, user) || true
+        }).catch((err) => {
+          return cb(err, null) || true
+        })
+      }
+      ))
+  }
+}

+ 42 - 90
server/configure.js

@@ -1,11 +1,12 @@
-'use strict'
+const path = require('path')
 
 
-module.exports = (port, spinner) => {
-  const path = require('path')
+/* global wiki */
 
 
-  const ROOTPATH = process.cwd()
-  const SERVERPATH = path.join(ROOTPATH, 'server')
-  const IS_DEBUG = process.env.NODE_ENV === 'development'
+module.exports = () => {
+  wiki.config.site = {
+    path: '',
+    title: 'Wiki.js'
+  }
 
 
   // ----------------------------------------
   // ----------------------------------------
   // Load modules
   // Load modules
@@ -26,28 +27,30 @@ module.exports = (port, spinner) => {
   // Define Express App
   // Define Express App
   // ----------------------------------------
   // ----------------------------------------
 
 
-  var app = express()
+  let app = express()
   app.use(compression())
   app.use(compression())
 
 
-  var server
+  let server
 
 
   // ----------------------------------------
   // ----------------------------------------
   // Public Assets
   // Public Assets
   // ----------------------------------------
   // ----------------------------------------
 
 
-  app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico')))
-  app.use(express.static(path.join(ROOTPATH, 'assets')))
+  app.use(favicon(path.join(wiki.ROOTPATH, 'assets', 'favicon.ico')))
+  app.use(express.static(path.join(wiki.ROOTPATH, 'assets')))
 
 
   // ----------------------------------------
   // ----------------------------------------
   // View Engine Setup
   // View Engine Setup
   // ----------------------------------------
   // ----------------------------------------
 
 
-  app.set('views', path.join(SERVERPATH, 'views'))
+  app.set('views', path.join(wiki.SERVERPATH, 'views'))
   app.set('view engine', 'pug')
   app.set('view engine', 'pug')
 
 
   app.use(bodyParser.json())
   app.use(bodyParser.json())
   app.use(bodyParser.urlencoded({ extended: false }))
   app.use(bodyParser.urlencoded({ extended: false }))
 
 
+  app.locals.config = wiki.config
+  app.locals.data = wiki.data
   app.locals._ = require('lodash')
   app.locals._ = require('lodash')
 
 
   // ----------------------------------------
   // ----------------------------------------
@@ -55,21 +58,8 @@ module.exports = (port, spinner) => {
   // ----------------------------------------
   // ----------------------------------------
 
 
   app.get('*', (req, res) => {
   app.get('*', (req, res) => {
-    let langs = []
-    let conf = {}
-    try {
-      langs = yaml.safeLoad(fs.readFileSync(path.join(SERVERPATH, 'app/data.yml'), 'utf8')).langs
-      conf = yaml.safeLoad(fs.readFileSync(path.join(ROOTPATH, 'config.yml'), 'utf8'))
-    } catch (err) {
-      console.error(err)
-    }
-    res.render('configure/index', {
-      langs,
-      conf,
-      runmode: {
-        staticPort: (process.env.WIKI_JS_HEROKU || process.env.WIKI_JS_DOCKER),
-        staticMongo: (!_.isNil(process.env.WIKI_JS_HEROKU))
-      }
+    fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => {
+      res.render('configure/index', { packageObj })
     })
     })
   })
   })
 
 
@@ -80,15 +70,15 @@ module.exports = (port, spinner) => {
     Promise.mapSeries([
     Promise.mapSeries([
       () => {
       () => {
         const semver = require('semver')
         const semver = require('semver')
-        if (!semver.satisfies(semver.clean(process.version), '>=6.9.0')) {
-          throw new Error('Node.js version is too old. Minimum is v6.6.0.')
+        if (!semver.satisfies(semver.clean(process.version), '>=6.11.1')) {
+          throw new Error('Node.js version is too old. Minimum is 6.11.1.')
         }
         }
-        return 'Node.js ' + process.version + ' detected. Minimum is v6.9.0.'
+        return 'Node.js ' + process.version + ' detected. Minimum is 6.11.1.'
       },
       },
       () => {
       () => {
         return Promise.try(() => {
         return Promise.try(() => {
           require('crypto')
           require('crypto')
-        }).catch(err => { // eslint-disable-line handle-callback-err
+        }).catch(err => {
           throw new Error('Crypto Node.js module is not available.')
           throw new Error('Crypto Node.js module is not available.')
         }).return('Node.js Crypto module is available.')
         }).return('Node.js Crypto module is available.')
       },
       },
@@ -102,24 +92,24 @@ module.exports = (port, spinner) => {
             }
             }
             let gitver = _.head(stdout.match(/[\d]+\.[\d]+(\.[\d]+)?/gi))
             let gitver = _.head(stdout.match(/[\d]+\.[\d]+(\.[\d]+)?/gi))
             if (!gitver || !semver.satisfies(semver.clean(gitver), '>=2.7.4')) {
             if (!gitver || !semver.satisfies(semver.clean(gitver), '>=2.7.4')) {
-              reject(new Error('Git version is too old. Minimum is v2.7.4.'))
+              reject(new Error('Git version is too old. Minimum is 2.7.4.'))
             }
             }
-            resolve('Git v' + gitver + ' detected. Minimum is v2.7.4.')
+            resolve('Git ' + gitver + ' detected. Minimum is 2.7.4.')
           })
           })
         })
         })
       },
       },
       () => {
       () => {
         const os = require('os')
         const os = require('os')
-        if (os.totalmem() < 1000 * 1000 * 768) {
-          throw new Error('Not enough memory. Minimum is 768 MB.')
+        if (os.totalmem() < 1000 * 1000 * 512) {
+          throw new Error('Not enough memory. Minimum is 512 MB.')
         }
         }
-        return _.round(os.totalmem() / (1024 * 1024)) + ' MB of system memory available. Minimum is 768 MB.'
+        return _.round(os.totalmem() / (1024 * 1024)) + ' MB of system memory available. Minimum is 512 MB.'
       },
       },
       () => {
       () => {
         let fs = require('fs')
         let fs = require('fs')
         return Promise.try(() => {
         return Promise.try(() => {
-          fs.accessSync(path.join(ROOTPATH, 'config.yml'), (fs.constants || fs).W_OK)
-        }).catch(err => { // eslint-disable-line handle-callback-err
+          fs.accessSync(path.join(wiki.ROOTPATH, 'config.yml'), (fs.constants || fs).W_OK)
+        }).catch(err => {
           throw new Error('config.yml file is not writable by Node.js process or was not created properly.')
           throw new Error('config.yml file is not writable by Node.js process or was not created properly.')
         }).return('config.yml is writable by the setup process.')
         }).return('config.yml is writable by the setup process.')
       }
       }
@@ -130,43 +120,6 @@ module.exports = (port, spinner) => {
     })
     })
   })
   })
 
 
-  /**
-   * Check the DB connection
-   */
-  app.post('/dbcheck', (req, res) => {
-    let mongo = require('mongodb').MongoClient
-    let mongoURI = cfgHelper.parseConfigValue(req.body.db)
-    mongo.connect(mongoURI, {
-      autoReconnect: false,
-      reconnectTries: 2,
-      reconnectInterval: 1000,
-      connectTimeoutMS: 5000,
-      socketTimeoutMS: 5000
-    }, (err, db) => {
-      if (err === null) {
-        // Try to create a test collection
-        db.createCollection('test', (err, results) => {
-          if (err === null) {
-            // Try to drop test collection
-            db.dropCollection('test', (err, results) => {
-              if (err === null) {
-                res.json({ ok: true })
-              } else {
-                res.json({ ok: false, error: 'Unable to delete test collection. Verify permissions. ' + err.message })
-              }
-              db.close()
-            })
-          } else {
-            res.json({ ok: false, error: 'Unable to create test collection. Verify permissions. ' + err.message })
-            db.close()
-          }
-        })
-      } else {
-        res.json({ ok: false, error: err.message })
-      }
-    })
-  })
-
   /**
   /**
    * Check the Git connection
    * Check the Git connection
    */
    */
@@ -174,8 +127,8 @@ module.exports = (port, spinner) => {
     const exec = require('execa')
     const exec = require('execa')
     const url = require('url')
     const url = require('url')
 
 
-    const dataDir = path.resolve(ROOTPATH, cfgHelper.parseConfigValue(req.body.pathData))
-    const gitDir = path.resolve(ROOTPATH, cfgHelper.parseConfigValue(req.body.pathRepo))
+    const dataDir = path.resolve(wiki.ROOTPATH, cfgHelper.parseConfigValue(req.body.pathData))
+    const gitDir = path.resolve(wiki.ROOTPATH, cfgHelper.parseConfigValue(req.body.pathRepo))
 
 
     let gitRemoteUrl = ''
     let gitRemoteUrl = ''
 
 
@@ -315,7 +268,7 @@ module.exports = (port, spinner) => {
           }
           }
         })
         })
       }),
       }),
-      fs.readFileAsync(path.join(ROOTPATH, 'config.yml'), 'utf8').then(confRaw => {
+      fs.readFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), 'utf8').then(confRaw => {
         let conf = yaml.safeLoad(confRaw)
         let conf = yaml.safeLoad(confRaw)
         conf.title = req.body.title
         conf.title = req.body.title
         conf.host = req.body.host
         conf.host = req.body.host
@@ -356,12 +309,12 @@ module.exports = (port, spinner) => {
         return crypto.randomBytesAsync(32).then(buf => {
         return crypto.randomBytesAsync(32).then(buf => {
           conf.sessionSecret = buf.toString('hex')
           conf.sessionSecret = buf.toString('hex')
           confRaw = yaml.safeDump(conf)
           confRaw = yaml.safeDump(conf)
-          return fs.writeFileAsync(path.join(ROOTPATH, 'config.yml'), confRaw)
+          return fs.writeFileAsync(path.join(wiki.ROOTPATH, 'config.yml'), confRaw)
         })
         })
       })
       })
     ).then(() => {
     ).then(() => {
       if (process.env.IS_HEROKU) {
       if (process.env.IS_HEROKU) {
-        return fs.outputJsonAsync(path.join(SERVERPATH, 'app/heroku.json'), { configured: true })
+        return fs.outputJsonAsync(path.join(wiki.SERVERPATH, 'app/heroku.json'), { configured: true })
       } else {
       } else {
         return true
         return true
       }
       }
@@ -377,7 +330,7 @@ module.exports = (port, spinner) => {
    */
    */
   app.post('/restart', (req, res) => {
   app.post('/restart', (req, res) => {
     res.status(204).end()
     res.status(204).end()
-    server.destroy(() => {
+    /* server.destroy(() => {
       spinner.text = 'Setup wizard terminated. Restarting in normal mode...'
       spinner.text = 'Setup wizard terminated. Restarting in normal mode...'
       _.delay(() => {
       _.delay(() => {
         const exec = require('execa')
         const exec = require('execa')
@@ -386,7 +339,7 @@ module.exports = (port, spinner) => {
           process.exit(0)
           process.exit(0)
         })
         })
       }, 1000)
       }, 1000)
-    })
+    }) */
   })
   })
 
 
   // ----------------------------------------
   // ----------------------------------------
@@ -403,21 +356,20 @@ module.exports = (port, spinner) => {
     res.status(err.status || 500)
     res.status(err.status || 500)
     res.send({
     res.send({
       message: err.message,
       message: err.message,
-      error: IS_DEBUG ? err : {}
+      error: wiki.IS_DEBUG ? err : {}
     })
     })
-    spinner.fail(err.message)
-    process.exit(1)
+    wiki.logger.error(err.message)
   })
   })
 
 
   // ----------------------------------------
   // ----------------------------------------
   // Start HTTP server
   // Start HTTP server
   // ----------------------------------------
   // ----------------------------------------
 
 
-  spinner.text = 'Starting HTTP server...'
+  wiki.logger.info(`HTTP Server on port: ${wiki.config.port}`)
 
 
-  app.set('port', port)
+  app.set('port', wiki.config.port)
   server = http.createServer(app)
   server = http.createServer(app)
-  server.listen(port)
+  server.listen(wiki.config.port)
 
 
   var openConnections = []
   var openConnections = []
 
 
@@ -443,10 +395,10 @@ module.exports = (port, spinner) => {
 
 
     switch (error.code) {
     switch (error.code) {
       case 'EACCES':
       case 'EACCES':
-        spinner.fail('Listening on port ' + port + ' requires elevated privileges!')
+        wiki.logger.error('Listening on port ' + wiki.config.port + ' requires elevated privileges!')
         return process.exit(1)
         return process.exit(1)
       case 'EADDRINUSE':
       case 'EADDRINUSE':
-        spinner.fail('Port ' + port + ' is already in use!')
+        wiki.logger.error('Port ' + wiki.config.port + ' is already in use!')
         return process.exit(1)
         return process.exit(1)
       default:
       default:
         throw error
         throw error
@@ -454,6 +406,6 @@ module.exports = (port, spinner) => {
   })
   })
 
 
   server.on('listening', () => {
   server.on('listening', () => {
-    spinner.text = 'Browse to http://localhost:' + port + ' to configure Wiki.js!'
+    wiki.logger.info('HTTP Server: RUNNING')
   })
   })
 }
 }

+ 32 - 32
server/controllers/admin.js

@@ -1,6 +1,6 @@
 'use strict'
 'use strict'
 
 
-/* global db, lang, rights, winston */
+/* global wiki */
 
 
 var express = require('express')
 var express = require('express')
 var router = express.Router()
 var router = express.Router()
@@ -33,14 +33,14 @@ router.post('/profile', (req, res) => {
     return res.render('error-forbidden')
     return res.render('error-forbidden')
   }
   }
 
 
-  return db.User.findById(req.user.id).then((usr) => {
+  return wiki.db.User.findById(req.user.id).then((usr) => {
     usr.name = _.trim(req.body.name)
     usr.name = _.trim(req.body.name)
     if (usr.provider === 'local' && req.body.password !== '********') {
     if (usr.provider === 'local' && req.body.password !== '********') {
       let nPwd = _.trim(req.body.password)
       let nPwd = _.trim(req.body.password)
       if (nPwd.length < 6) {
       if (nPwd.length < 6) {
         return Promise.reject(new Error('New Password too short!'))
         return Promise.reject(new Error('New Password too short!'))
       } else {
       } else {
-        return db.User.hashPassword(nPwd).then((pwd) => {
+        return wiki.db.User.hashPassword(nPwd).then((pwd) => {
           usr.password = pwd
           usr.password = pwd
           return usr.save()
           return usr.save()
         })
         })
@@ -61,9 +61,9 @@ router.get('/stats', (req, res) => {
   }
   }
 
 
   Promise.all([
   Promise.all([
-    db.Entry.count(),
-    db.UplFile.count(),
-    db.User.count()
+    wiki.db.Entry.count(),
+    wiki.db.UplFile.count(),
+    wiki.db.User.count()
   ]).spread((totalEntries, totalUploads, totalUsers) => {
   ]).spread((totalEntries, totalUploads, totalUsers) => {
     return res.render('pages/admin/stats', {
     return res.render('pages/admin/stats', {
       totalEntries, totalUploads, totalUsers, adminTab: 'stats'
       totalEntries, totalUploads, totalUsers, adminTab: 'stats'
@@ -78,7 +78,7 @@ router.get('/users', (req, res) => {
     return res.render('error-forbidden')
     return res.render('error-forbidden')
   }
   }
 
 
-  db.User.find({})
+  wiki.db.User.find({})
     .select('-password -rights')
     .select('-password -rights')
     .sort('name email')
     .sort('name email')
     .exec().then((usrs) => {
     .exec().then((usrs) => {
@@ -95,7 +95,7 @@ router.get('/users/:id', (req, res) => {
     return res.render('error-forbidden')
     return res.render('error-forbidden')
   }
   }
 
 
-  db.User.findById(req.params.id)
+  wiki.db.User.findById(req.params.id)
     .select('-password -providerId')
     .select('-password -providerId')
     .exec().then((usr) => {
     .exec().then((usr) => {
       let usrOpts = {
       let usrOpts = {
@@ -137,12 +137,12 @@ router.post('/users/create', (req, res) => {
     return res.status(400).json({ msg: 'Name is missing' })
     return res.status(400).json({ msg: 'Name is missing' })
   }
   }
 
 
-  db.User.findOne({ email: nUsr.email, provider: nUsr.provider }).then(exUsr => {
+  wiki.db.User.findOne({ email: nUsr.email, provider: nUsr.provider }).then(exUsr => {
     if (exUsr) {
     if (exUsr) {
       return res.status(400).json({ msg: 'User already exists!' }) || true
       return res.status(400).json({ msg: 'User already exists!' }) || true
     }
     }
 
 
-    let pwdGen = (nUsr.provider === 'local') ? db.User.hashPassword(nUsr.password) : Promise.resolve(true)
+    let pwdGen = (nUsr.provider === 'local') ? wiki.db.User.hashPassword(nUsr.password) : Promise.resolve(true)
     return pwdGen.then(nPwd => {
     return pwdGen.then(nPwd => {
       if (nUsr.provider !== 'local') {
       if (nUsr.provider !== 'local') {
         nUsr.password = ''
         nUsr.password = ''
@@ -158,37 +158,37 @@ router.post('/users/create', (req, res) => {
         deny: false
         deny: false
       }]
       }]
 
 
-      return db.User.create(nUsr).then(() => {
+      return wiki.db.User.create(nUsr).then(() => {
         return res.json({ ok: true })
         return res.json({ ok: true })
       })
       })
     }).catch(err => {
     }).catch(err => {
-      winston.warn(err)
+      wiki.logger.warn(err)
       return res.status(500).json({ msg: err })
       return res.status(500).json({ msg: err })
     })
     })
   }).catch(err => {
   }).catch(err => {
-    winston.warn(err)
+    wiki.logger.warn(err)
     return res.status(500).json({ msg: err })
     return res.status(500).json({ msg: err })
   })
   })
 })
 })
 
 
 router.post('/users/:id', (req, res) => {
 router.post('/users/:id', (req, res) => {
   if (!res.locals.rights.manage) {
   if (!res.locals.rights.manage) {
-    return res.status(401).json({ msg: lang.t('errors:unauthorized') })
+    return res.status(401).json({ msg: wiki.lang.t('errors:unauthorized') })
   }
   }
 
 
   if (!validator.isMongoId(req.params.id)) {
   if (!validator.isMongoId(req.params.id)) {
-    return res.status(400).json({ msg: lang.t('errors:invaliduserid') })
+    return res.status(400).json({ msg: wiki.lang.t('errors:invaliduserid') })
   }
   }
 
 
-  return db.User.findById(req.params.id).then((usr) => {
+  return wiki.db.User.findById(req.params.id).then((usr) => {
     usr.name = _.trim(req.body.name)
     usr.name = _.trim(req.body.name)
     usr.rights = JSON.parse(req.body.rights)
     usr.rights = JSON.parse(req.body.rights)
     if (usr.provider === 'local' && req.body.password !== '********') {
     if (usr.provider === 'local' && req.body.password !== '********') {
       let nPwd = _.trim(req.body.password)
       let nPwd = _.trim(req.body.password)
       if (nPwd.length < 6) {
       if (nPwd.length < 6) {
-        return Promise.reject(new Error(lang.t('errors:newpasswordtooshort')))
+        return Promise.reject(new Error(wiki.lang.t('errors:newpasswordtooshort')))
       } else {
       } else {
-        return db.User.hashPassword(nPwd).then((pwd) => {
+        return wiki.db.User.hashPassword(nPwd).then((pwd) => {
           usr.password = pwd
           usr.password = pwd
           return usr.save()
           return usr.save()
         })
         })
@@ -199,7 +199,7 @@ router.post('/users/:id', (req, res) => {
   }).then((usr) => {
   }).then((usr) => {
     // Update guest rights for future requests
     // Update guest rights for future requests
     if (usr.provider === 'local' && usr.email === 'guest') {
     if (usr.provider === 'local' && usr.email === 'guest') {
-      rights.guest = usr
+      wiki.rights.guest = usr
     }
     }
     return usr
     return usr
   }).then(() => {
   }).then(() => {
@@ -214,14 +214,14 @@ router.post('/users/:id', (req, res) => {
  */
  */
 router.delete('/users/:id', (req, res) => {
 router.delete('/users/:id', (req, res) => {
   if (!res.locals.rights.manage) {
   if (!res.locals.rights.manage) {
-    return res.status(401).json({ msg: lang.t('errors:unauthorized') })
+    return res.status(401).json({ msg: wiki.lang.t('errors:unauthorized') })
   }
   }
 
 
   if (!validator.isMongoId(req.params.id)) {
   if (!validator.isMongoId(req.params.id)) {
-    return res.status(400).json({ msg: lang.t('errors:invaliduserid') })
+    return res.status(400).json({ msg: wiki.lang.t('errors:invaliduserid') })
   }
   }
 
 
-  return db.User.findByIdAndRemove(req.params.id).then(() => {
+  return wiki.db.User.findByIdAndRemove(req.params.id).then(() => {
     return res.json({ ok: true })
     return res.json({ ok: true })
   }).catch((err) => {
   }).catch((err) => {
     res.status(500).json({ ok: false, msg: err.message })
     res.status(500).json({ ok: false, msg: err.message })
@@ -249,7 +249,7 @@ router.get('/system', (req, res) => {
     cwd: process.cwd()
     cwd: process.cwd()
   }
   }
 
 
-  fs.readJsonAsync(path.join(ROOTPATH, 'package.json')).then(packageObj => {
+  fs.readJsonAsync(path.join(wiki.ROOTPATH, 'package.json')).then(packageObj => {
     axios.get('https://api.github.com/repos/Requarks/wiki/releases/latest').then(resp => {
     axios.get('https://api.github.com/repos/Requarks/wiki/releases/latest').then(resp => {
       let sysversion = {
       let sysversion = {
         current: 'v' + packageObj.version,
         current: 'v' + packageObj.version,
@@ -259,7 +259,7 @@ router.get('/system', (req, res) => {
 
 
       res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion })
       res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion })
     }).catch(err => {
     }).catch(err => {
-      winston.warn(err)
+      wiki.logger.warn(err)
       res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion: { current: 'v' + packageObj.version } })
       res.render('pages/admin/system', { adminTab: 'system', hostInfo, sysversion: { current: 'v' + packageObj.version } })
     })
     })
   })
   })
@@ -287,19 +287,19 @@ router.post('/theme', (req, res) => {
     return res.render('error-forbidden')
     return res.render('error-forbidden')
   }
   }
 
 
-  if (!validator.isIn(req.body.primary, appdata.colors)) {
+  if (!validator.isIn(req.body.primary, wiki.data.colors)) {
     return res.status(406).json({ msg: 'Primary color is invalid.' })
     return res.status(406).json({ msg: 'Primary color is invalid.' })
-  } else if (!validator.isIn(req.body.alt, appdata.colors)) {
+  } else if (!validator.isIn(req.body.alt, wiki.data.colors)) {
     return res.status(406).json({ msg: 'Alternate color is invalid.' })
     return res.status(406).json({ msg: 'Alternate color is invalid.' })
-  } else if (!validator.isIn(req.body.footer, appdata.colors)) {
+  } else if (!validator.isIn(req.body.footer, wiki.data.colors)) {
     return res.status(406).json({ msg: 'Footer color is invalid.' })
     return res.status(406).json({ msg: 'Footer color is invalid.' })
   }
   }
 
 
-  appconfig.theme.primary = req.body.primary
-  appconfig.theme.alt = req.body.alt
-  appconfig.theme.footer = req.body.footer
-  appconfig.theme.code.dark = req.body.codedark === 'true'
-  appconfig.theme.code.colorize = req.body.codecolorize === 'true'
+  wiki.config.theme.primary = req.body.primary
+  wiki.config.theme.alt = req.body.alt
+  wiki.config.theme.footer = req.body.footer
+  wiki.config.theme.code.dark = req.body.codedark === 'true'
+  wiki.config.theme.code.colorize = req.body.codecolorize === 'true'
 
 
   return res.json({ msg: 'OK' })
   return res.json({ msg: 'OK' })
 })
 })

+ 28 - 27
server/controllers/auth.js

@@ -1,19 +1,19 @@
-'use strict'
-
-/* global db, lang */
+/* global wiki */
 
 
 const Promise = require('bluebird')
 const Promise = require('bluebird')
 const express = require('express')
 const express = require('express')
 const router = express.Router()
 const router = express.Router()
-const passport = require('passport')
 const ExpressBrute = require('express-brute')
 const ExpressBrute = require('express-brute')
-const ExpressBruteMongooseStore = require('express-brute-mongoose')
+const ExpressBruteRedisStore = require('express-brute-redis')
 const moment = require('moment')
 const moment = require('moment')
+const _ = require('lodash')
 
 
 /**
 /**
  * Setup Express-Brute
  * Setup Express-Brute
  */
  */
-const EBstore = new ExpressBruteMongooseStore(db.Bruteforce)
+const EBstore = new ExpressBruteRedisStore({
+  client: wiki.redis
+})
 const bruteforce = new ExpressBrute(EBstore, {
 const bruteforce = new ExpressBrute(EBstore, {
   freeRetries: 5,
   freeRetries: 5,
   minWait: 60 * 1000,
   minWait: 60 * 1000,
@@ -22,8 +22,8 @@ const bruteforce = new ExpressBrute(EBstore, {
   failCallback (req, res, next, nextValidRequestDate) {
   failCallback (req, res, next, nextValidRequestDate) {
     req.flash('alert', {
     req.flash('alert', {
       class: 'error',
       class: 'error',
-      title: lang.t('auth:errors.toomanyattempts'),
-      message: lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }),
+      title: wiki.lang.t('auth:errors.toomanyattempts'),
+      message: wiki.lang.t('auth:errors.toomanyattemptsmsg', { time: moment(nextValidRequestDate).fromNow() }),
       iconClass: 'fa-times'
       iconClass: 'fa-times'
     })
     })
     res.redirect('/login')
     res.redirect('/login')
@@ -35,23 +35,24 @@ const bruteforce = new ExpressBrute(EBstore, {
  */
  */
 router.get('/login', function (req, res, next) {
 router.get('/login', function (req, res, next) {
   res.render('auth/login', {
   res.render('auth/login', {
-    usr: res.locals.usr
+    authStrategies: _.reject(wiki.auth.strategies, { key: 'local' }),
+    hasMultipleStrategies: Object.keys(wiki.config.auth.strategies).length > 1
   })
   })
 })
 })
 
 
 router.post('/login', bruteforce.prevent, function (req, res, next) {
 router.post('/login', bruteforce.prevent, function (req, res, next) {
   new Promise((resolve, reject) => {
   new Promise((resolve, reject) => {
     // [1] LOCAL AUTHENTICATION
     // [1] LOCAL AUTHENTICATION
-    passport.authenticate('local', function (err, user, info) {
+    wiki.auth.passport.authenticate('local', function (err, user, info) {
       if (err) { return reject(err) }
       if (err) { return reject(err) }
       if (!user) { return reject(new Error('INVALID_LOGIN')) }
       if (!user) { return reject(new Error('INVALID_LOGIN')) }
       resolve(user)
       resolve(user)
     })(req, res, next)
     })(req, res, next)
   }).catch({ message: 'INVALID_LOGIN' }, err => {
   }).catch({ message: 'INVALID_LOGIN' }, err => {
-    if (appconfig.auth.ldap && appconfig.auth.ldap.enabled) {
+    if (_.has(wiki.config.auth.strategy, 'ldap')) {
       // [2] LDAP AUTHENTICATION
       // [2] LDAP AUTHENTICATION
       return new Promise((resolve, reject) => {
       return new Promise((resolve, reject) => {
-        passport.authenticate('ldapauth', function (err, user, info) {
+        wiki.auth.passport.authenticate('ldapauth', function (err, user, info) {
           if (err) { return reject(err) }
           if (err) { return reject(err) }
           if (info && info.message) { return reject(new Error(info.message)) }
           if (info && info.message) { return reject(new Error(info.message)) }
           if (!user) { return reject(new Error('INVALID_LOGIN')) }
           if (!user) { return reject(new Error('INVALID_LOGIN')) }
@@ -73,13 +74,13 @@ router.post('/login', bruteforce.prevent, function (req, res, next) {
     // LOGIN FAIL
     // LOGIN FAIL
     if (err.message === 'INVALID_LOGIN') {
     if (err.message === 'INVALID_LOGIN') {
       req.flash('alert', {
       req.flash('alert', {
-        title: lang.t('auth:errors.invalidlogin'),
-        message: lang.t('auth:errors.invalidloginmsg')
+        title: wiki.lang.t('auth:errors.invalidlogin'),
+        message: wiki.lang.t('auth:errors.invalidloginmsg')
       })
       })
       return res.redirect('/login')
       return res.redirect('/login')
     } else {
     } else {
       req.flash('alert', {
       req.flash('alert', {
-        title: lang.t('auth:errors.loginerror'),
+        title: wiki.lang.t('auth:errors.loginerror'),
         message: err.message
         message: err.message
       })
       })
       return res.redirect('/login')
       return res.redirect('/login')
@@ -91,19 +92,19 @@ router.post('/login', bruteforce.prevent, function (req, res, next) {
  * Social Login
  * Social Login
  */
  */
 
 
-router.get('/login/ms', passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }))
-router.get('/login/google', passport.authenticate('google', { scope: ['profile', 'email'] }))
-router.get('/login/facebook', passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
-router.get('/login/github', passport.authenticate('github', { scope: ['user:email'] }))
-router.get('/login/slack', passport.authenticate('slack', { scope: ['identity.basic', 'identity.email'] }))
-router.get('/login/azure', passport.authenticate('azure_ad_oauth2'))
+router.get('/login/ms', wiki.auth.passport.authenticate('windowslive', { scope: ['wl.signin', 'wl.basic', 'wl.emails'] }))
+router.get('/login/google', wiki.auth.passport.authenticate('google', { scope: ['profile', 'email'] }))
+router.get('/login/facebook', wiki.auth.passport.authenticate('facebook', { scope: ['public_profile', 'email'] }))
+router.get('/login/github', wiki.auth.passport.authenticate('github', { scope: ['user:email'] }))
+router.get('/login/slack', wiki.auth.passport.authenticate('slack', { scope: ['identity.basic', 'identity.email'] }))
+router.get('/login/azure', wiki.auth.passport.authenticate('azure_ad_oauth2'))
 
 
-router.get('/login/ms/callback', passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/google/callback', passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/github/callback', passport.authenticate('github', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/slack/callback', passport.authenticate('slack', { failureRedirect: '/login', successRedirect: '/' }))
-router.get('/login/azure/callback', passport.authenticate('azure_ad_oauth2', { failureRedirect: '/login', successRedirect: '/' }))
+router.get('/login/ms/callback', wiki.auth.passport.authenticate('windowslive', { failureRedirect: '/login', successRedirect: '/' }))
+router.get('/login/google/callback', wiki.auth.passport.authenticate('google', { failureRedirect: '/login', successRedirect: '/' }))
+router.get('/login/facebook/callback', wiki.auth.passport.authenticate('facebook', { failureRedirect: '/login', successRedirect: '/' }))
+router.get('/login/github/callback', wiki.auth.passport.authenticate('github', { failureRedirect: '/login', successRedirect: '/' }))
+router.get('/login/slack/callback', wiki.auth.passport.authenticate('slack', { failureRedirect: '/login', successRedirect: '/' }))
+router.get('/login/azure/callback', wiki.auth.passport.authenticate('azure_ad_oauth2', { failureRedirect: '/login', successRedirect: '/' }))
 
 
 /**
 /**
  * Logout
  * Logout

+ 16 - 13
server/controllers/uploads.js

@@ -1,6 +1,9 @@
 'use strict'
 'use strict'
 
 
-/* global git, lang, lcdata, upl */
+/* global wiki */
+
+module.exports = false
+return
 
 
 const express = require('express')
 const express = require('express')
 const router = express.Router()
 const router = express.Router()
@@ -12,7 +15,7 @@ const fs = Promise.promisifyAll(require('fs-extra'))
 const path = require('path')
 const path = require('path')
 const _ = require('lodash')
 const _ = require('lodash')
 
 
-const validPathRe = new RegExp('^([a-z0-9/-' + appdata.regex.cjk + appdata.regex.arabic + ']+\\.[a-z0-9]+)$')
+const validPathRe = new RegExp('^([a-z0-9/-' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']+\\.[a-z0-9]+)$')
 const validPathThumbsRe = new RegExp('^([a-z0-9]+\\.png)$')
 const validPathThumbsRe = new RegExp('^([a-z0-9]+\\.png)$')
 
 
 // ==========================================
 // ==========================================
@@ -28,7 +31,7 @@ router.get('/t/*', (req, res, next) => {
   // todo: Authentication-based access
   // todo: Authentication-based access
 
 
   res.sendFile(fileName, {
   res.sendFile(fileName, {
-    root: lcdata.getThumbsPath(),
+    root: wiki.disk.getThumbsPath(),
     dotfiles: 'deny'
     dotfiles: 'deny'
   }, (err) => {
   }, (err) => {
     if (err) {
     if (err) {
@@ -37,12 +40,12 @@ router.get('/t/*', (req, res, next) => {
   })
   })
 })
 })
 
 
-router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
+router.post('/img', wiki.disk.uploadImgHandler, (req, res, next) => {
   let destFolder = _.chain(req.body.folder).trim().toLower().value()
   let destFolder = _.chain(req.body.folder).trim().toLower().value()
 
 
-  upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
+  wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
     if (!destFolderPath) {
     if (!destFolderPath) {
-      res.json({ ok: false, msg: lang.t('errors:invalidfolder') })
+      res.json({ ok: false, msg: wiki.lang.t('errors:invalidfolder') })
       return true
       return true
     }
     }
 
 
@@ -50,7 +53,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
       let destFilename = ''
       let destFilename = ''
       let destFilePath = ''
       let destFilePath = ''
 
 
-      return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
+      return wiki.disk.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
         destFilename = fname
         destFilename = fname
         destFilePath = path.resolve(destFolderPath, destFilename)
         destFilePath = path.resolve(destFolderPath, destFilename)
 
 
@@ -60,7 +63,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
 
 
         let mimeInfo = fileType(buf)
         let mimeInfo = fileType(buf)
         if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
         if (!_.includes(['image/png', 'image/jpeg', 'image/gif', 'image/webp'], mimeInfo.mime)) {
-          return Promise.reject(new Error(lang.t('errors:invalidfiletype')))
+          return Promise.reject(new Error(wiki.lang.t('errors:invalidfiletype')))
         }
         }
         return true
         return true
       }).then(() => {
       }).then(() => {
@@ -94,12 +97,12 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
   })
   })
 })
 })
 
 
-router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
+router.post('/file', wiki.disk.uploadFileHandler, (req, res, next) => {
   let destFolder = _.chain(req.body.folder).trim().toLower().value()
   let destFolder = _.chain(req.body.folder).trim().toLower().value()
 
 
-  upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
+  wiki.upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
     if (!destFolderPath) {
     if (!destFolderPath) {
-      res.json({ ok: false, msg: lang.t('errors:invalidfolder') })
+      res.json({ ok: false, msg: wiki.lang.t('errors:invalidfolder') })
       return true
       return true
     }
     }
 
 
@@ -107,7 +110,7 @@ router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
       let destFilename = ''
       let destFilename = ''
       let destFilePath = ''
       let destFilePath = ''
 
 
-      return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
+      return wiki.disk.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
         destFilename = fname
         destFilename = fname
         destFilePath = path.resolve(destFolderPath, destFilename)
         destFilePath = path.resolve(destFolderPath, destFilename)
 
 
@@ -150,7 +153,7 @@ router.get('/*', (req, res, next) => {
   // todo: Authentication-based access
   // todo: Authentication-based access
 
 
   res.sendFile(fileName, {
   res.sendFile(fileName, {
-    root: git.getRepoPath() + '/uploads/',
+    root: wiki.git.getRepoPath() + '/uploads/',
     dotfiles: 'deny'
     dotfiles: 'deny'
   }, (err) => {
   }, (err) => {
     if (err) {
     if (err) {

+ 19 - 252
server/index.js

@@ -1,276 +1,43 @@
-'use strict'
-
 // ===========================================
 // ===========================================
 // Wiki.js
 // Wiki.js
-// 1.0.0
 // Licensed under AGPLv3
 // Licensed under AGPLv3
 // ===========================================
 // ===========================================
 
 
 const path = require('path')
 const path = require('path')
-const ROOTPATH = process.cwd()
-const SERVERPATH = path.join(ROOTPATH, 'server')
-
-global.ROOTPATH = ROOTPATH
-global.SERVERPATH = SERVERPATH
-const IS_DEBUG = process.env.NODE_ENV === 'development'
+const cluster = require('cluster')
+
+let wiki = {
+  IS_DEBUG: process.env.NODE_ENV === 'development',
+  IS_MASTER: cluster.isMaster,
+  ROOTPATH: process.cwd(),
+  SERVERPATH: path.join(process.cwd(), 'server'),
+  configSvc: require('./modules/config'),
+  kernel: require('./modules/kernel')
+}
+global.wiki = wiki
 
 
 process.env.VIPS_WARNING = false
 process.env.VIPS_WARNING = false
 
 
-// if (IS_DEBUG) {
+// if (wiki.IS_DEBUG) {
 //   require('@glimpse/glimpse').init()
 //   require('@glimpse/glimpse').init()
 // }
 // }
 
 
-let appconf = require('./libs/config')()
-global.appconfig = appconf.config
-global.appdata = appconf.data
-
-// ----------------------------------------
-// Load Winston
-// ----------------------------------------
-
-global.winston = require('./libs/logger')(IS_DEBUG, 'SERVER')
-global.winston.info('Wiki.js is initializing...')
-
-// ----------------------------------------
-// Load global modules
-// ----------------------------------------
-
-global.lcdata = require('./libs/local').init()
-global.db = require('./libs/db').init()
-global.entries = require('./libs/entries').init()
-global.git = require('./libs/git').init(false)
-global.lang = require('i18next')
-global.mark = require('./libs/markdown')
-global.search = require('./libs/search').init()
-global.upl = require('./libs/uploads').init()
-
-// ----------------------------------------
-// Load modules
-// ----------------------------------------
-
-const autoload = require('auto-load')
-const bodyParser = require('body-parser')
-const compression = require('compression')
-const cookieParser = require('cookie-parser')
-const express = require('express')
-const favicon = require('serve-favicon')
-const flash = require('connect-flash')
-const fork = require('child_process').fork
-const http = require('http')
-const i18nBackend = require('i18next-node-fs-backend')
-const passport = require('passport')
-const passportSocketIo = require('passport.socketio')
-const session = require('express-session')
-const SessionMongoStore = require('connect-mongo')(session)
-const graceful = require('node-graceful')
-const socketio = require('socket.io')
-
-var mw = autoload(path.join(SERVERPATH, '/middlewares'))
-var ctrl = autoload(path.join(SERVERPATH, '/controllers'))
-
-// ----------------------------------------
-// Define Express App
-// ----------------------------------------
-
-const app = express()
-global.app = app
-app.use(compression())
-
-// ----------------------------------------
-// Security
-// ----------------------------------------
-
-app.use(mw.security)
-
-// ----------------------------------------
-// Public Assets
-// ----------------------------------------
-
-app.use(favicon(path.join(ROOTPATH, 'assets', 'favicon.ico')))
-app.use(express.static(path.join(ROOTPATH, 'assets'), {
-  index: false,
-  maxAge: '7d'
-}))
+wiki.configSvc.init()
 
 
 // ----------------------------------------
 // ----------------------------------------
-// Passport Authentication
+// Init Logger
 // ----------------------------------------
 // ----------------------------------------
 
 
-require('./libs/auth')(passport)
-global.rights = require('./libs/rights')
-global.rights.init()
+wiki.logger = require('./modules/logger').init()
 
 
-let sessionStore = new SessionMongoStore({
-  mongooseConnection: global.db.connection,
-  touchAfter: 15
-})
-
-app.use(cookieParser())
-app.use(session({
-  name: 'wikijs.sid',
-  store: sessionStore,
-  secret: appconfig.sessionSecret,
-  resave: false,
-  saveUninitialized: false
-}))
-app.use(flash())
-app.use(passport.initialize())
-app.use(passport.session())
-
-// ----------------------------------------
-// SEO
 // ----------------------------------------
 // ----------------------------------------
-
-app.use(mw.seo)
-
-// ----------------------------------------
-// Localization Engine
-// ----------------------------------------
-
-global.lang
-  .use(i18nBackend)
-  .init({
-    load: 'languageOnly',
-    ns: ['common', 'admin', 'auth', 'errors', 'git'],
-    defaultNS: 'common',
-    saveMissing: false,
-    preload: [appconfig.lang],
-    lng: appconfig.lang,
-    fallbackLng: 'en',
-    backend: {
-      loadPath: path.join(SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
-    }
-  })
-
+// Init DB
 // ----------------------------------------
 // ----------------------------------------
-// View Engine Setup
-// ----------------------------------------
-
-app.set('views', path.join(SERVERPATH, 'views'))
-app.set('view engine', 'pug')
-
-app.use(bodyParser.json({ limit: '1mb' }))
-app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' }))
-
-// ----------------------------------------
-// View accessible data
-// ----------------------------------------
-
-app.locals._ = require('lodash')
-app.locals.t = global.lang.t.bind(global.lang)
-app.locals.moment = require('moment')
-app.locals.moment.locale(appconfig.lang)
-app.locals.appconfig = appconfig
-app.use(mw.flash)
-
-// ----------------------------------------
-// Controllers
-// ----------------------------------------
-
-app.use('/', ctrl.auth)
-
-app.use('/uploads', mw.auth, ctrl.uploads)
-app.use('/admin', mw.auth, ctrl.admin)
-app.use('/', mw.auth, ctrl.pages)
-
-// ----------------------------------------
-// Error handling
-// ----------------------------------------
-
-app.use(function (req, res, next) {
-  var err = new Error('Not Found')
-  err.status = 404
-  next(err)
-})
-
-app.use(function (err, req, res, next) {
-  res.status(err.status || 500)
-  res.render('error', {
-    message: err.message,
-    error: IS_DEBUG ? err : {}
-  })
-})
-
-// ----------------------------------------
-// Start HTTP server
-// ----------------------------------------
-
-global.winston.info('Starting HTTP/WS server on port ' + appconfig.port + '...')
-
-app.set('port', appconfig.port)
-var server = http.createServer(app)
-var io = socketio(server)
-
-server.listen(appconfig.port)
-server.on('error', (error) => {
-  if (error.syscall !== 'listen') {
-    throw error
-  }
-
-  // handle specific listen errors with friendly messages
-  switch (error.code) {
-    case 'EACCES':
-      global.winston.error('Listening on port ' + appconfig.port + ' requires elevated privileges!')
-      return process.exit(1)
-    case 'EADDRINUSE':
-      global.winston.error('Port ' + appconfig.port + ' is already in use!')
-      return process.exit(1)
-    default:
-      throw error
-  }
-})
-
-server.on('listening', () => {
-  global.winston.info('HTTP/WS server started successfully! [RUNNING]')
-})
-
-// ----------------------------------------
-// WebSocket
-// ----------------------------------------
-
-io.use(passportSocketIo.authorize({
-  key: 'wikijs.sid',
-  store: sessionStore,
-  secret: appconfig.sessionSecret,
-  cookieParser,
-  success: (data, accept) => {
-    accept()
-  },
-  fail: (data, message, error, accept) => {
-    accept()
-  }
-}))
-
-io.on('connection', ctrl.ws)
-
-// ----------------------------------------
-// Start child processes
-// ----------------------------------------
-
-let bgAgent = fork(path.join(SERVERPATH, 'agent.js'))
-
-bgAgent.on('message', m => {
-  if (!m.action) {
-    return
-  }
 
 
-  switch (m.action) {
-    case 'searchAdd':
-      global.search.add(m.content)
-      break
-  }
-})
+wiki.db = require('./modules/db').init()
 
 
 // ----------------------------------------
 // ----------------------------------------
-// Graceful shutdown
+// Start Kernel
 // ----------------------------------------
 // ----------------------------------------
 
 
-graceful.on('exit', () => {
-  global.winston.info('- SHUTTING DOWN - Terminating Background Agent...')
-  bgAgent.kill()
-  global.winston.info('- SHUTTING DOWN - Performing git sync...')
-  return global.git.resync().then(() => {
-    global.winston.info('- SHUTTING DOWN - Git sync successful. Now safe to exit.')
-    process.exit()
-  })
-})
+wiki.kernel.init()

+ 0 - 90
server/init.js

@@ -1,90 +0,0 @@
-'use strict'
-
-const Promise = require('bluebird')
-const fs = Promise.promisifyAll(require('fs-extra'))
-const pm2 = Promise.promisifyAll(require('pm2'))
-const ora = require('ora')
-const path = require('path')
-
-const ROOTPATH = process.cwd()
-
-module.exports = {
-  /**
-   * Detect the most appropriate start mode
-   */
-  startDetect: function () {
-    if (process.env.WIKI_JS_HEROKU) {
-      return this.startInHerokuMode()
-    } else {
-      return this.startInBackgroundMode()
-    }
-  },
-  /**
-   * Start in background mode
-   */
-  startInBackgroundMode: function () {
-    let spinner = ora('Initializing...').start()
-    return fs.emptyDirAsync(path.join(ROOTPATH, './logs')).then(() => {
-      return pm2.connectAsync().then(() => {
-        return pm2.startAsync({
-          name: 'wiki',
-          script: 'server',
-          cwd: ROOTPATH,
-          output: path.join(ROOTPATH, './logs/wiki-output.log'),
-          error: path.join(ROOTPATH, './logs/wiki-error.log'),
-          minUptime: 5000,
-          maxRestarts: 5
-        }).then(() => {
-          spinner.succeed('Wiki.js has started successfully.')
-        }).finally(() => {
-          pm2.disconnect()
-        })
-      })
-    }).catch(err => {
-      spinner.fail(err)
-      process.exit(1)
-    })
-  },
-  /**
-   * Start in Heroku mode
-   */
-  startInHerokuMode: function () {
-    console.warn('Incorrect command on Heroku, use instead: node server')
-    process.exit(1)
-  },
-  /**
-   * Stop Wiki.js process(es)
-   */
-  stop () {
-    let spinner = ora('Shutting down Wiki.js...').start()
-    return pm2.connectAsync().then(() => {
-      return pm2.stopAsync('wiki').then(() => {
-        spinner.succeed('Wiki.js has stopped successfully.')
-      }).finally(() => {
-        pm2.disconnect()
-      })
-    }).catch(err => {
-      spinner.fail(err)
-      process.exit(1)
-    })
-  },
-  /**
-   * Restart Wiki.js process(es)
-   */
-  restart: function () {
-    let self = this
-    return self.stop().delay(1000).then(() => {
-      self.startDetect()
-    })
-  },
-  /**
-   * Start the web-based configuration wizard
-   *
-   * @param {Number} port Port to bind the HTTP server on
-   */
-  configure (port) {
-    port = port || 3000
-    let spinner = ora('Initializing interactive setup...').start()
-    require('./configure')(port, spinner)
-  }
-}

+ 0 - 261
server/libs/auth.js

@@ -1,261 +0,0 @@
-'use strict'
-
-/* global appconfig, appdata, db, lang, winston */
-
-const fs = require('fs')
-
-module.exports = function (passport) {
-  // Serialization user methods
-
-  passport.serializeUser(function (user, done) {
-    done(null, user._id)
-  })
-
-  passport.deserializeUser(function (id, done) {
-    db.User.findById(id).then((user) => {
-      if (user) {
-        done(null, user)
-      } else {
-        done(new Error(lang.t('auth:errors:usernotfound')), null)
-      }
-      return true
-    }).catch((err) => {
-      done(err, null)
-    })
-  })
-
-  // Local Account
-
-  if (appconfig.auth.local && appconfig.auth.local.enabled) {
-    const LocalStrategy = require('passport-local').Strategy
-    passport.use('local',
-      new LocalStrategy({
-        usernameField: 'email',
-        passwordField: 'password'
-      }, (uEmail, uPassword, done) => {
-        db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => {
-          if (user) {
-            return user.validatePassword(uPassword).then(() => {
-              return done(null, user) || true
-            }).catch((err) => {
-              return done(err, null)
-            })
-          } else {
-            return done(new Error('INVALID_LOGIN'), null)
-          }
-        }).catch((err) => {
-          done(err, null)
-        })
-      }
-      ))
-  }
-
-  // Google ID
-
-  if (appconfig.auth.google && appconfig.auth.google.enabled) {
-    const GoogleStrategy = require('passport-google-oauth20').Strategy
-    passport.use('google',
-      new GoogleStrategy({
-        clientID: appconfig.auth.google.clientId,
-        clientSecret: appconfig.auth.google.clientSecret,
-        callbackURL: appconfig.host + '/login/google/callback'
-      }, (accessToken, refreshToken, profile, cb) => {
-        db.User.processProfile(profile).then((user) => {
-          return cb(null, user) || true
-        }).catch((err) => {
-          return cb(err, null) || true
-        })
-      }
-      ))
-  }
-
-  // Microsoft Accounts
-
-  if (appconfig.auth.microsoft && appconfig.auth.microsoft.enabled) {
-    const WindowsLiveStrategy = require('passport-windowslive').Strategy
-    passport.use('windowslive',
-      new WindowsLiveStrategy({
-        clientID: appconfig.auth.microsoft.clientId,
-        clientSecret: appconfig.auth.microsoft.clientSecret,
-        callbackURL: appconfig.host + '/login/ms/callback'
-      }, function (accessToken, refreshToken, profile, cb) {
-        db.User.processProfile(profile).then((user) => {
-          return cb(null, user) || true
-        }).catch((err) => {
-          return cb(err, null) || true
-        })
-      }
-      ))
-  }
-
-  // Facebook
-
-  if (appconfig.auth.facebook && appconfig.auth.facebook.enabled) {
-    const FacebookStrategy = require('passport-facebook').Strategy
-    passport.use('facebook',
-      new FacebookStrategy({
-        clientID: appconfig.auth.facebook.clientId,
-        clientSecret: appconfig.auth.facebook.clientSecret,
-        callbackURL: appconfig.host + '/login/facebook/callback',
-        profileFields: ['id', 'displayName', 'email']
-      }, function (accessToken, refreshToken, profile, cb) {
-        db.User.processProfile(profile).then((user) => {
-          return cb(null, user) || true
-        }).catch((err) => {
-          return cb(err, null) || true
-        })
-      }
-      ))
-  }
-
-  // GitHub
-
-  if (appconfig.auth.github && appconfig.auth.github.enabled) {
-    const GitHubStrategy = require('passport-github2').Strategy
-    passport.use('github',
-      new GitHubStrategy({
-        clientID: appconfig.auth.github.clientId,
-        clientSecret: appconfig.auth.github.clientSecret,
-        callbackURL: appconfig.host + '/login/github/callback',
-        scope: ['user:email']
-      }, (accessToken, refreshToken, profile, cb) => {
-        db.User.processProfile(profile).then((user) => {
-          return cb(null, user) || true
-        }).catch((err) => {
-          return cb(err, null) || true
-        })
-      }
-      ))
-  }
-
-  // Slack
-
-  if (appconfig.auth.slack && appconfig.auth.slack.enabled) {
-    const SlackStrategy = require('passport-slack').Strategy
-    passport.use('slack',
-      new SlackStrategy({
-        clientID: appconfig.auth.slack.clientId,
-        clientSecret: appconfig.auth.slack.clientSecret,
-        callbackURL: appconfig.host + '/login/slack/callback'
-      }, (accessToken, refreshToken, profile, cb) => {
-        db.User.processProfile(profile).then((user) => {
-          return cb(null, user) || true
-        }).catch((err) => {
-          return cb(err, null) || true
-        })
-      }
-      ))
-  }
-
-  // LDAP
-
-  if (appconfig.auth.ldap && appconfig.auth.ldap.enabled) {
-    const LdapStrategy = require('passport-ldapauth').Strategy
-    passport.use('ldapauth',
-      new LdapStrategy({
-        server: {
-          url: appconfig.auth.ldap.url,
-          bindDn: appconfig.auth.ldap.bindDn,
-          bindCredentials: appconfig.auth.ldap.bindCredentials,
-          searchBase: appconfig.auth.ldap.searchBase,
-          searchFilter: appconfig.auth.ldap.searchFilter,
-          searchAttributes: ['displayName', 'name', 'cn', 'mail'],
-          tlsOptions: (appconfig.auth.ldap.tlsEnabled) ? {
-            ca: [
-              fs.readFileSync(appconfig.auth.ldap.tlsCertPath)
-            ]
-          } : {}
-        },
-        usernameField: 'email',
-        passReqToCallback: false
-      }, (profile, cb) => {
-        profile.provider = 'ldap'
-        profile.id = profile.dn
-        db.User.processProfile(profile).then((user) => {
-          return cb(null, user) || true
-        }).catch((err) => {
-          return cb(err, null) || true
-        })
-      }
-      ))
-  }
-
-  // AZURE AD
-
-  if (appconfig.auth.azure && appconfig.auth.azure.enabled) {
-    const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2').Strategy
-    const jwt = require('jsonwebtoken')
-    passport.use('azure_ad_oauth2',
-      new AzureAdOAuth2Strategy({
-        clientID: appconfig.auth.azure.clientId,
-        clientSecret: appconfig.auth.azure.clientSecret,
-        callbackURL: appconfig.host + '/login/azure/callback',
-        resource: appconfig.auth.azure.resource,
-        tenant: appconfig.auth.azure.tenant
-      }, (accessToken, refreshToken, params, profile, cb) => {
-        let waadProfile = jwt.decode(params.id_token)
-        waadProfile.id = waadProfile.oid
-        waadProfile.provider = 'azure'
-        db.User.processProfile(waadProfile).then((user) => {
-          return cb(null, user) || true
-        }).catch((err) => {
-          return cb(err, null) || true
-        })
-      }
-      ))
-  }
-
-  // Create users for first-time
-
-  db.onReady.then(() => {
-    return db.User.findOne({ provider: 'local', email: 'guest' }).then((c) => {
-      if (c < 1) {
-        // Create guest account
-
-        return db.User.create({
-          provider: 'local',
-          email: 'guest',
-          name: 'Guest',
-          password: '',
-          rights: [{
-            role: 'read',
-            path: '/',
-            exact: false,
-            deny: !appconfig.public
-          }]
-        }).then(() => {
-          winston.info('[AUTH] Guest account created successfully!')
-        }).catch((err) => {
-          winston.error('[AUTH] An error occured while creating guest account:')
-          winston.error(err)
-        })
-      }
-    }).then(() => {
-      if (process.env.WIKI_JS_HEROKU) {
-        return db.User.findOne({ provider: 'local', email: process.env.WIKI_ADMIN_EMAIL }).then((c) => {
-          if (c < 1) {
-            // Create root admin account (HEROKU ONLY)
-
-            return db.User.create({
-              provider: 'local',
-              email: process.env.WIKI_ADMIN_EMAIL,
-              name: 'Administrator',
-              password: '$2a$04$MAHRw785Xe/Jd5kcKzr3D.VRZDeomFZu2lius4gGpZZ9cJw7B7Mna', // admin123 (default)
-              rights: [{
-                role: 'admin',
-                path: '/',
-                exact: false,
-                deny: false
-              }]
-            }).then(() => {
-              winston.info('[AUTH] Root admin account created successfully!')
-            }).catch((err) => {
-              winston.error('[AUTH] An error occured while creating root admin account:')
-              winston.error(err)
-            })
-          } else { return true }
-        })
-      } else { return true }
-    })
-  })
-}

+ 0 - 68
server/libs/config.js

@@ -1,68 +0,0 @@
-'use strict'
-
-const fs = require('fs')
-const yaml = require('js-yaml')
-const _ = require('lodash')
-const path = require('path')
-const cfgHelper = require('../helpers/config')
-
-/**
- * Load Application Configuration
- *
- * @param      {Object}  confPaths  Path to the configuration files
- * @return     {Object}  Application Configuration
- */
-module.exports = (confPaths) => {
-  confPaths = _.defaults(confPaths, {
-    config: path.join(ROOTPATH, 'config.yml'),
-    data: path.join(SERVERPATH, 'app/data.yml'),
-    dataRegex: path.join(SERVERPATH, 'app/regex.js')
-  })
-
-  let appconfig = {}
-  let appdata = {}
-
-  try {
-    appconfig = yaml.safeLoad(
-      cfgHelper.parseConfigValue(
-        fs.readFileSync(confPaths.config, 'utf8')
-      )
-    )
-
-    appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8'))
-    appdata.regex = require(confPaths.dataRegex)
-  } catch (ex) {
-    console.error(ex)
-    process.exit(1)
-  }
-
-  // Merge with defaults
-
-  appconfig = _.defaultsDeep(appconfig, appdata.defaults.config)
-
-  // Check port
-
-  if (appconfig.port < 1) {
-    appconfig.port = process.env.PORT || 80
-  }
-
-  // Convert booleans
-
-  appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true')
-
-  // List authentication strategies
-
-  appconfig.authStrategies = {
-    list: _.filter(appconfig.auth, ['enabled', true]),
-    socialEnabled: (_.chain(appconfig.auth).omit(['local', 'ldap']).filter(['enabled', true]).value().length > 0)
-  }
-  if (appconfig.authStrategies.list.length < 1) {
-    console.error(new Error('You must enable at least 1 authentication strategy!'))
-    process.exit(1)
-  }
-
-  return {
-    config: appconfig,
-    data: appdata
-  }
-}

+ 0 - 63
server/libs/db.js

@@ -1,63 +0,0 @@
-'use strict'
-
-/* global ROOTPATH, appconfig, winston */
-
-const modb = require('mongoose')
-const fs = require('fs')
-const path = require('path')
-const _ = require('lodash')
-
-/**
- * MongoDB module
- *
- * @return     {Object}  MongoDB wrapper instance
- */
-module.exports = {
-
-  /**
-   * Initialize DB
-   *
-   * @return     {Object}  DB instance
-   */
-  init() {
-    let self = this
-
-    let dbModelsPath = path.join(SERVERPATH, 'models')
-
-    modb.Promise = require('bluebird')
-
-    // Event handlers
-
-    modb.connection.on('error', err => {
-      winston.error('Failed to connect to MongoDB instance.')
-      return err
-    })
-    modb.connection.once('open', function () {
-      winston.log('Connected to MongoDB instance.')
-    })
-
-    // Store connection handle
-
-    self.connection = modb.connection
-    self.ObjectId = modb.Types.ObjectId
-
-    // Load DB Models
-
-    fs
-      .readdirSync(dbModelsPath)
-      .filter(function (file) {
-        return (file.indexOf('.') !== 0)
-      })
-      .forEach(function (file) {
-        let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0]))
-        self[modelName] = require(path.join(dbModelsPath, file))
-      })
-
-    // Connect
-
-    self.onReady = modb.connect(appconfig.db, { useMongoClient: true })
-
-    return self
-  }
-
-}

+ 0 - 77
server/libs/logger.js

@@ -1,77 +0,0 @@
-'use strict'
-
-module.exports = (isDebug, processName) => {
-  let winston = require('winston')
-
-  if (typeof processName === 'undefined') {
-    processName = 'SERVER'
-  }
-
-  // Console
-
-  let logger = new (winston.Logger)({
-    level: (isDebug) ? 'debug' : 'info',
-    transports: [
-      new (winston.transports.Console)({
-        level: (isDebug) ? 'debug' : 'info',
-        prettyPrint: true,
-        colorize: true,
-        silent: false,
-        timestamp: true
-      })
-    ]
-  })
-
-  logger.filters.push((level, msg) => {
-    return '[' + processName + '] ' + msg
-  })
-
-  // External services
-
-  if (appconfig.externalLogging.bugsnag) {
-    const bugsnagTransport = require('./winston-transports/bugsnag')
-    logger.add(bugsnagTransport, {
-      level: 'warn',
-      key: appconfig.externalLogging.bugsnag
-    })
-  }
-
-  if (appconfig.externalLogging.loggly) {
-    require('winston-loggly-bulk')
-    logger.add(winston.transports.Loggly, {
-      token: appconfig.externalLogging.loggly.token,
-      subdomain: appconfig.externalLogging.loggly.subdomain,
-      tags: ['wiki-js'],
-      level: 'warn',
-      json: true
-    })
-  }
-
-  if (appconfig.externalLogging.papertrail) {
-    require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions
-    logger.add(winston.transports.Papertrail, {
-      host: appconfig.externalLogging.papertrail.host,
-      port: appconfig.externalLogging.papertrail.port,
-      level: 'warn',
-      program: 'wiki.js'
-    })
-  }
-
-  if (appconfig.externalLogging.rollbar) {
-    const rollbarTransport = require('./winston-transports/rollbar')
-    logger.add(rollbarTransport, {
-      level: 'warn',
-      key: appconfig.externalLogging.rollbar
-    })
-  }
-
-  if (appconfig.externalLogging.sentry) {
-    const sentryTransport = require('./winston-transports/sentry')
-    logger.add(sentryTransport, {
-      level: 'warn',
-      key: appconfig.externalLogging.sentry
-    })
-  }
-
-  return logger
-}

+ 0 - 81
server/libs/search-index/index.js

@@ -1,81 +0,0 @@
-const bunyan = require('bunyan')
-const level = require('levelup')
-const down = require('memdown')
-const SearchIndexAdder = require('search-index-adder')
-const SearchIndexSearcher = require('search-index-searcher')
-
-module.exports = function (givenOptions, moduleReady) {
-  const optionsLoaded = function (err, SearchIndex) {
-    const siUtil = require('./siUtil.js')(SearchIndex.options)
-    if (err) return moduleReady(err)
-    SearchIndex.close = siUtil.close
-    SearchIndex.countDocs = siUtil.countDocs
-    getAdder(SearchIndex, adderLoaded)
-  }
-
-  const adderLoaded = function (err, SearchIndex) {
-    if (err) return moduleReady(err)
-    getSearcher(SearchIndex, searcherLoaded)
-  }
-
-  const searcherLoaded = function (err, SearchIndex) {
-    if (err) return moduleReady(err)
-    return moduleReady(err, SearchIndex)
-  }
-
-  getOptions(givenOptions, optionsLoaded)
-}
-
-const getAdder = function (SearchIndex, done) {
-  SearchIndexAdder(SearchIndex.options, function (err, searchIndexAdder) {
-    SearchIndex.add = searchIndexAdder.add
-    SearchIndex.callbackyAdd = searchIndexAdder.concurrentAdd // deprecated
-    SearchIndex.concurrentAdd = searchIndexAdder.concurrentAdd
-    SearchIndex.createWriteStream = searchIndexAdder.createWriteStream
-    SearchIndex.dbWriteStream = searchIndexAdder.dbWriteStream
-    SearchIndex.defaultPipeline = searchIndexAdder.defaultPipeline
-    SearchIndex.del = searchIndexAdder.deleter
-    SearchIndex.deleteStream = searchIndexAdder.deleteStream
-    SearchIndex.flush = searchIndexAdder.flush
-    done(err, SearchIndex)
-  })
-}
-
-const getSearcher = function (SearchIndex, done) {
-  SearchIndexSearcher(SearchIndex.options, function (err, searchIndexSearcher) {
-    SearchIndex.availableFields = searchIndexSearcher.availableFields
-    SearchIndex.buckets = searchIndexSearcher.bucketStream
-    SearchIndex.categorize = searchIndexSearcher.categoryStream
-    SearchIndex.dbReadStream = searchIndexSearcher.dbReadStream
-    SearchIndex.get = searchIndexSearcher.get
-    SearchIndex.match = searchIndexSearcher.match
-    SearchIndex.scan = searchIndexSearcher.scan
-    SearchIndex.search = searchIndexSearcher.search
-    SearchIndex.totalHits = searchIndexSearcher.totalHits
-    done(err, SearchIndex)
-  })
-}
-
-const getOptions = function (options, done) {
-  var SearchIndex = {}
-  SearchIndex.options = Object.assign({}, {
-    indexPath: 'si',
-    keySeparator: '○',
-    logLevel: 'error'
-  }, options)
-  options.log = bunyan.createLogger({
-    name: 'search-index',
-    level: options.logLevel
-  })
-  if (!options.indexes) {
-    level(SearchIndex.options.indexPath || 'si', {
-      valueEncoding: 'json',
-      db: down
-    }, function (err, db) {
-      SearchIndex.options.indexes = db
-      return done(err, SearchIndex)
-    })
-  } else {
-    return done(null, SearchIndex)
-  }
-}

+ 0 - 36
server/libs/search-index/siUtil.js

@@ -1,36 +0,0 @@
-'use strict'
-
-module.exports = function (siOptions) {
-  var siUtil = {}
-
-  siUtil.countDocs = function (callback) {
-    var count = 0
-    const gte = 'DOCUMENT' + siOptions.keySeparator
-    const lte = 'DOCUMENT' + siOptions.keySeparator + siOptions.keySeparator
-    siOptions.indexes.createReadStream({gte: gte, lte: lte})
-      .on('data', function (data) {
-        count++
-      })
-      .on('error', function (err) {
-        return callback(err, null)
-      })
-      .on('end', function () {
-        return callback(null, count)
-      })
-  }
-
-  siUtil.close = function (callback) {
-    siOptions.indexes.close(function (err) {
-      while (!siOptions.indexes.isClosed()) {
-        // log not always working here- investigate
-        if (siOptions.log) siOptions.log.info('closing...')
-      }
-      if (siOptions.indexes.isClosed()) {
-        if (siOptions.log) siOptions.log.info('closed...')
-        callback(err)
-      }
-    })
-  }
-
-  return siUtil
-}

+ 1 - 1
server/locales/ja/auth.json

@@ -13,7 +13,7 @@
     "invalidlogin": "不正なログイン",
     "invalidlogin": "不正なログイン",
     "invalidloginmsg": "Eメール又はパスワードが無効です。",
     "invalidloginmsg": "Eメール又はパスワードが無効です。",
     "invaliduseremail": "無効なユーザーEメール",
     "invaliduseremail": "無効なユーザーEメール",
-    "lognerror": "ログインエラー",
+    "loginerror": "ログインエラー",
     "notyetauthorized": "まだこのサイトにログインする権限がありません。",
     "notyetauthorized": "まだこのサイトにログインする権限がありません。",
     "toomanyattempts": "試行回数が多すぎます",
     "toomanyattempts": "試行回数が多すぎます",
     "toomanyattemptsmsg": "短期間に失敗した試行回数が多すぎます。{{time}}にもう一度お試しください。",
     "toomanyattemptsmsg": "短期間に失敗した試行回数が多すぎます。{{time}}にもう一度お試しください。",

+ 186 - 0
server/master.js

@@ -0,0 +1,186 @@
+/* global wiki */
+
+module.exports = () => {
+  // ----------------------------------------
+  // Load global modules
+  // ----------------------------------------
+
+  wiki.auth = require('./modules/auth').init()
+  wiki.disk = require('./modules/disk').init()
+  wiki.docs = require('./modules/documents').init()
+  wiki.git = require('./modules/git').init(false)
+  wiki.lang = require('./modules/localization').init()
+  wiki.mark = require('./modules/markdown')
+  wiki.search = require('./modules/search').init()
+  wiki.upl = require('./modules/uploads').init()
+
+  // ----------------------------------------
+  // Load modules
+  // ----------------------------------------
+
+  const autoload = require('auto-load')
+  const bodyParser = require('body-parser')
+  const compression = require('compression')
+  const cookieParser = require('cookie-parser')
+  const express = require('express')
+  const favicon = require('serve-favicon')
+  const flash = require('connect-flash')
+  const http = require('http')
+  const path = require('path')
+  const session = require('express-session')
+  const SessionRedisStore = require('connect-redis')(session)
+  const graceful = require('node-graceful')
+  const graphqlApollo = require('apollo-server-express')
+  const graphqlSchema = require('./modules/graphql')
+
+  var mw = autoload(path.join(wiki.SERVERPATH, '/middlewares'))
+  var ctrl = autoload(path.join(wiki.SERVERPATH, '/controllers'))
+
+  // ----------------------------------------
+  // Define Express App
+  // ----------------------------------------
+
+  const app = express()
+  wiki.app = app
+  app.use(compression())
+
+  // ----------------------------------------
+  // Security
+  // ----------------------------------------
+
+  app.use(mw.security)
+
+  // ----------------------------------------
+  // Public Assets
+  // ----------------------------------------
+
+  app.use(favicon(path.join(wiki.ROOTPATH, 'assets', 'favicon.ico')))
+  app.use(express.static(path.join(wiki.ROOTPATH, 'assets'), {
+    index: false,
+    maxAge: '7d'
+  }))
+
+  // ----------------------------------------
+  // Passport Authentication
+  // ----------------------------------------
+
+  let sessionStore = new SessionRedisStore({
+    client: wiki.redis
+  })
+
+  app.use(cookieParser())
+  app.use(session({
+    name: 'wikijs.sid',
+    store: sessionStore,
+    secret: wiki.config.site.sessionSecret,
+    resave: false,
+    saveUninitialized: false
+  }))
+  app.use(flash())
+  app.use(wiki.auth.passport.initialize())
+  app.use(wiki.auth.passport.session())
+
+  // ----------------------------------------
+  // SEO
+  // ----------------------------------------
+
+  app.use(mw.seo)
+
+  // ----------------------------------------
+  // View Engine Setup
+  // ----------------------------------------
+
+  app.set('views', path.join(wiki.SERVERPATH, 'views'))
+  app.set('view engine', 'pug')
+
+  app.use(bodyParser.json({ limit: '1mb' }))
+  app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' }))
+
+  // ----------------------------------------
+  // View accessible data
+  // ----------------------------------------
+
+  app.locals.basedir = wiki.ROOTPATH
+  app.locals._ = require('lodash')
+  app.locals.t = wiki.lang.engine.t.bind(wiki.lang)
+  app.locals.moment = require('moment')
+  app.locals.moment.locale(wiki.config.site.lang)
+  app.locals.config = wiki.config
+  app.use(mw.flash)
+
+  // ----------------------------------------
+  // Controllers
+  // ----------------------------------------
+
+  app.use('/', ctrl.auth)
+
+  app.use('/graphql', graphqlApollo.graphqlExpress({ schema: graphqlSchema }))
+  app.use('/graphiql', graphqlApollo.graphiqlExpress({ endpointURL: '/graphql' }))
+  // app.use('/uploads', mw.auth, ctrl.uploads)
+  app.use('/admin', mw.auth, ctrl.admin)
+  app.use('/', mw.auth, ctrl.pages)
+
+  // ----------------------------------------
+  // Error handling
+  // ----------------------------------------
+
+  app.use(function (req, res, next) {
+    var err = new Error('Not Found')
+    err.status = 404
+    next(err)
+  })
+
+  app.use(function (err, req, res, next) {
+    res.status(err.status || 500)
+    res.render('error', {
+      message: err.message,
+      error: wiki.IS_DEBUG ? err : {}
+    })
+  })
+
+  // ----------------------------------------
+  // Start HTTP server
+  // ----------------------------------------
+
+  wiki.logger.info(`HTTP Server on port: ${wiki.config.port}`)
+
+  app.set('port', wiki.config.port)
+  let server = http.createServer(app)
+
+  server.listen(wiki.config.port)
+  server.on('error', (error) => {
+    if (error.syscall !== 'listen') {
+      throw error
+    }
+
+    // handle specific listen errors with friendly messages
+    switch (error.code) {
+      case 'EACCES':
+        wiki.logger.error('Listening on port ' + wiki.config.port + ' requires elevated privileges!')
+        return process.exit(1)
+      case 'EADDRINUSE':
+        wiki.logger.error('Port ' + wiki.config.port + ' is already in use!')
+        return process.exit(1)
+      default:
+        throw error
+    }
+  })
+
+  server.on('listening', () => {
+    wiki.logger.info('HTTP Server: RUNNING')
+  })
+
+  // ----------------------------------------
+  // Graceful shutdown
+  // ----------------------------------------
+
+  graceful.on('exit', () => {
+    wiki.logger.info('- SHUTTING DOWN - Performing git sync...')
+    return global.git.resync().then(() => {
+      wiki.logger.info('- SHUTTING DOWN - Git sync successful. Now safe to exit.')
+      process.exit()
+    })
+  })
+
+  return true
+}

+ 1 - 1
server/middlewares/flash.js

@@ -9,7 +9,7 @@
  * @return     {any}               void
  * @return     {any}               void
  */
  */
 module.exports = (req, res, next) => {
 module.exports = (req, res, next) => {
-  res.locals.appflash = req.flash('alert')
+  res.locals.flash = req.flash('alert')
 
 
   next()
   next()
 }
 }

+ 1 - 3
server/middlewares/security.js

@@ -1,7 +1,5 @@
 'use strict'
 'use strict'
 
 
-/* global app */
-
 /**
 /**
  * Security Middleware
  * Security Middleware
  *
  *
@@ -12,7 +10,7 @@
  */
  */
 module.exports = function (req, res, next) {
 module.exports = function (req, res, next) {
   // -> Disable X-Powered-By
   // -> Disable X-Powered-By
-  app.disable('x-powered-by')
+  req.app.disable('x-powered-by')
 
 
   // -> Disable Frame Embedding
   // -> Disable Frame Embedding
   res.set('X-Frame-Options', 'deny')
   res.set('X-Frame-Options', 'deny')

+ 16 - 0
server/models/_relations.js

@@ -0,0 +1,16 @@
+/**
+ * Associate DB Model relations
+ */
+module.exports = db => {
+  db.User.belongsToMany(db.Group, { through: 'userGroups' })
+  db.Group.belongsToMany(db.User, { through: 'userGroups' })
+  db.Group.hasMany(db.Right)
+  db.Right.belongsTo(db.Group)
+  db.Document.belongsToMany(db.Tag, { through: 'documentTags' })
+  db.Document.hasMany(db.Comment)
+  db.Tag.belongsToMany(db.Document, { through: 'documentTags' })
+  db.File.belongsTo(db.Folder)
+  db.Folder.hasMany(db.File)
+  db.Comment.belongsTo(db.Document)
+  db.Comment.belongsTo(db.User, { as: 'author' })
+}

+ 0 - 20
server/models/bruteforce.js

@@ -1,20 +0,0 @@
-'use strict'
-
-const Mongoose = require('mongoose')
-
-/**
- * BruteForce schema
- *
- * @type       {<Mongoose.Schema>}
- */
-var bruteForceSchema = Mongoose.Schema({
-  _id: { type: String, index: 1 },
-  data: {
-    count: Number,
-    lastRequest: Date,
-    firstRequest: Date
-  },
-  expires: { type: Date, index: { expires: '1d' } }
-})
-
-module.exports = Mongoose.model('Bruteforce', bruteForceSchema)

+ 16 - 0
server/models/comment.js

@@ -0,0 +1,16 @@
+/**
+ * Comment schema
+ */
+module.exports = (sequelize, DataTypes) => {
+  let commentSchema = sequelize.define('comment', {
+    content: {
+      type: DataTypes.STRING,
+      allowNull: false
+    }
+  }, {
+    timestamps: true,
+    version: true
+  })
+
+  return commentSchema
+}

+ 64 - 0
server/models/document.js

@@ -0,0 +1,64 @@
+/**
+ * Document schema
+ */
+module.exports = (sequelize, DataTypes) => {
+  let documentSchema = sequelize.define('setting', {
+    path: {
+      type: DataTypes.STRING,
+      allowNull: false
+    },
+    title: {
+      type: DataTypes.STRING,
+      allowNull: false,
+      validate: {
+        len: [2, 255]
+      }
+    },
+    subtitle: {
+      type: DataTypes.STRING,
+      allowNull: true,
+      defaultValue: ''
+    },
+    parentPath: {
+      type: DataTypes.STRING,
+      allowNull: true,
+      defaultValue: ''
+    },
+    parentTitle: {
+      type: DataTypes.STRING,
+      allowNull: true,
+      defaultValue: ''
+    },
+    isDirectory: {
+      type: DataTypes.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    },
+    isEntry: {
+      type: DataTypes.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    },
+    isDraft: {
+      type: DataTypes.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    },
+    searchContent: {
+      type: DataTypes.TEXT,
+      allowNull: true,
+      defaultValue: ''
+    }
+  }, {
+    timestamps: true,
+    version: true,
+    indexes: [
+      {
+        unique: true,
+        fields: ['path']
+      }
+    ]
+  })
+
+  return documentSchema
+}

+ 0 - 42
server/models/entry.js

@@ -1,42 +0,0 @@
-'use strict'
-
-const Mongoose = require('mongoose')
-
-/**
- * Entry schema
- *
- * @type       {<Mongoose.Schema>}
- */
-var entrySchema = Mongoose.Schema({
-  _id: String,
-
-  title: {
-    type: String,
-    required: true,
-    minlength: 2
-  },
-  subtitle: {
-    type: String,
-    default: ''
-  },
-  parentTitle: {
-    type: String,
-    default: ''
-  },
-  parentPath: {
-    type: String,
-    default: ''
-  },
-  isDirectory: {
-    type: Boolean,
-    default: false
-  },
-  isEntry: {
-    type: Boolean,
-    default: false
-  }
-}, {
-  timestamps: {}
-})
-
-module.exports = Mongoose.model('Entry', entrySchema)

+ 42 - 0
server/models/file.js

@@ -0,0 +1,42 @@
+/**
+ * File schema
+ */
+module.exports = (sequelize, DataTypes) => {
+  let fileSchema = sequelize.define('file', {
+    category: {
+      type: DataTypes.ENUM('binary', 'image'),
+      allowNull: false,
+      defaultValue: 'binary'
+    },
+    mime: {
+      type: DataTypes.STRING,
+      allowNull: false,
+      defaultValue: 'application/octet-stream'
+    },
+    extra: {
+      type: DataTypes.JSONB,
+      allowNull: true
+    },
+    filename: {
+      type: DataTypes.STRING,
+      allowNull: false
+    },
+    basename: {
+      type: DataTypes.STRING,
+      allowNull: false
+    },
+    filesize: {
+      type: DataTypes.INTEGER,
+      allowNull: false,
+      validate: {
+        isInt: true,
+        min: 0
+      }
+    }
+  }, {
+    timestamps: true,
+    version: true
+  })
+
+  return fileSchema
+}

+ 22 - 0
server/models/folder.js

@@ -0,0 +1,22 @@
+/**
+ * Folder schema
+ */
+module.exports = (sequelize, DataTypes) => {
+  let folderSchema = sequelize.define('folder', {
+    name: {
+      type: DataTypes.STRING,
+      allowNull: false
+    }
+  }, {
+    timestamps: true,
+    version: true,
+    indexes: [
+      {
+        unique: true,
+        fields: ['name']
+      }
+    ]
+  })
+
+  return folderSchema
+}

+ 16 - 0
server/models/group.js

@@ -0,0 +1,16 @@
+/**
+ * Group schema
+ */
+module.exports = (sequelize, DataTypes) => {
+  let groupSchema = sequelize.define('group', {
+    name: {
+      type: DataTypes.STRING,
+      allowNull: false
+    }
+  }, {
+    timestamps: true,
+    version: true
+  })
+
+  return groupSchema
+}

+ 36 - 0
server/models/right.js

@@ -0,0 +1,36 @@
+/**
+ * Right schema
+ */
+module.exports = (sequelize, DataTypes) => {
+  let rightSchema = sequelize.define('right', {
+    path: {
+      type: DataTypes.STRING,
+      allowNull: false
+    },
+    role: {
+      type: DataTypes.ENUM('read', 'write', 'manage'),
+      allowNull: false,
+      defaultValue: 'read'
+    },
+    exact: {
+      type: DataTypes.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    },
+    allow: {
+      type: DataTypes.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    }
+  }, {
+    timestamps: true,
+    version: true,
+    indexes: [
+      {
+        fields: ['path']
+      }
+    ]
+  })
+
+  return rightSchema
+}

+ 22 - 18
server/models/setting.js

@@ -1,22 +1,26 @@
-'use strict'
-
-const Mongoose = require('mongoose')
-
 /**
 /**
  * Settings schema
  * Settings schema
- *
- * @type       {<Mongoose.Schema>}
  */
  */
-var settingSchema = Mongoose.Schema({
-  key: {
-    type: String,
-    required: true,
-    index: true
-  },
-  value: {
-    type: String,
-    required: true
-  }
-}, { timestamps: {} })
+module.exports = (sequelize, DataTypes) => {
+  let settingSchema = sequelize.define('setting', {
+    key: {
+      type: DataTypes.STRING,
+      allowNull: false
+    },
+    config: {
+      type: DataTypes.JSONB,
+      allowNull: false
+    }
+  }, {
+    timestamps: true,
+    version: true,
+    indexes: [
+      {
+        unique: true,
+        fields: ['key']
+      }
+    ]
+  })
 
 
-module.exports = Mongoose.model('Setting', settingSchema)
+  return settingSchema
+}

+ 22 - 0
server/models/tag.js

@@ -0,0 +1,22 @@
+/**
+ * Tags schema
+ */
+module.exports = (sequelize, DataTypes) => {
+  let tagSchema = sequelize.define('tag', {
+    key: {
+      type: DataTypes.STRING,
+      allowNull: false
+    }
+  }, {
+    timestamps: true,
+    version: true,
+    indexes: [
+      {
+        unique: true,
+        fields: ['key']
+      }
+    ]
+  })
+
+  return tagSchema
+}

+ 0 - 46
server/models/upl-file.js

@@ -1,46 +0,0 @@
-'use strict'
-
-const Mongoose = require('mongoose')
-
-/**
- * Upload File schema
- *
- * @type       {<Mongoose.Schema>}
- */
-var uplFileSchema = Mongoose.Schema({
-
-  _id: String,
-
-  category: {
-    type: String,
-    required: true,
-    default: 'binary'
-  },
-  mime: {
-    type: String,
-    required: true,
-    default: 'application/octet-stream'
-  },
-  extra: {
-    type: Object
-  },
-  folder: {
-    type: String,
-    ref: 'UplFolder'
-  },
-  filename: {
-    type: String,
-    required: true
-  },
-  basename: {
-    type: String,
-    required: true
-  },
-  filesize: {
-    type: Number,
-    required: true
-  }
-
-}, { timestamps: {} })
-
-module.exports = Mongoose.model('UplFile', uplFileSchema)

+ 0 - 21
server/models/upl-folder.js

@@ -1,21 +0,0 @@
-'use strict'
-
-const Mongoose = require('mongoose')
-
-/**
- * Upload Folder schema
- *
- * @type       {<Mongoose.Schema>}
- */
-var uplFolderSchema = Mongoose.Schema({
-
-  _id: String,
-
-  name: {
-    type: String,
-    index: true
-  }
-
-}, { timestamps: {} })
-
-module.exports = Mongoose.model('UplFolder', uplFolderSchema)

+ 104 - 93
server/models/user.js

@@ -1,109 +1,120 @@
-'use strict'
+/* global wiki */
 
 
-/* global db, lang */
-
-const Mongoose = require('mongoose')
 const Promise = require('bluebird')
 const Promise = require('bluebird')
 const bcrypt = require('bcryptjs-then')
 const bcrypt = require('bcryptjs-then')
 const _ = require('lodash')
 const _ = require('lodash')
 
 
 /**
 /**
  * Users schema
  * Users schema
- *
- * @type       {<Mongoose.Schema>}
  */
  */
-var userSchema = Mongoose.Schema({
-
-  email: {
-    type: String,
-    required: true,
-    index: true
-  },
-
-  provider: {
-    type: String,
-    required: true
-  },
-
-  providerId: {
-    type: String
-  },
-
-  password: {
-    type: String
-  },
-
-  name: {
-    type: String
-  },
-
-  rights: [{
-    role: String,
-    path: String,
-    exact: Boolean,
-    deny: Boolean
-  }]
-
-}, { timestamps: {} })
+module.exports = (sequelize, DataTypes) => {
+  let userSchema = sequelize.define('user', {
+    email: {
+      type: DataTypes.STRING,
+      allowNull: false,
+      validate: {
+        isEmail: true
+      }
+    },
+    provider: {
+      type: DataTypes.STRING,
+      allowNull: false
+    },
+    providerId: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+    password: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+    name: {
+      type: DataTypes.STRING,
+      allowNull: true
+    },
+    role: {
+      type: DataTypes.ENUM('admin', 'user', 'guest'),
+      allowNull: false
+    },
+    tfaIsActive: {
+      type: DataTypes.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    },
+    tfaSecret: {
+      type: DataTypes.STRING,
+      allowNull: true
+    }
+  }, {
+    timestamps: true,
+    version: true,
+    indexes: [
+      {
+        unique: true,
+        fields: ['provider', 'email']
+      }
+    ]
+  })
 
 
-userSchema.statics.processProfile = (profile) => {
-  let primaryEmail = ''
-  if (_.isArray(profile.emails)) {
-    let e = _.find(profile.emails, ['primary', true])
-    primaryEmail = (e) ? e.value : _.first(profile.emails).value
-  } else if (_.isString(profile.email) && profile.email.length > 5) {
-    primaryEmail = profile.email
-  } else if (_.isString(profile.mail) && profile.mail.length > 5) {
-    primaryEmail = profile.mail
-  } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
-    primaryEmail = profile.user.email
-  } else {
-    return Promise.reject(new Error(lang.t('auth:errors.invaliduseremail')))
+  userSchema.prototype.validatePassword = function (rawPwd) {
+    return bcrypt.compare(rawPwd, this.password).then((isValid) => {
+      return (isValid) ? true : Promise.reject(new Error(wiki.lang.t('auth:errors:invalidlogin')))
+    })
   }
   }
 
 
-  profile.provider = _.lowerCase(profile.provider)
-  primaryEmail = _.toLower(primaryEmail)
+  userSchema.processProfile = (profile) => {
+    let primaryEmail = ''
+    if (_.isArray(profile.emails)) {
+      let e = _.find(profile.emails, ['primary', true])
+      primaryEmail = (e) ? e.value : _.first(profile.emails).value
+    } else if (_.isString(profile.email) && profile.email.length > 5) {
+      primaryEmail = profile.email
+    } else if (_.isString(profile.mail) && profile.mail.length > 5) {
+      primaryEmail = profile.mail
+    } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
+      primaryEmail = profile.user.email
+    } else {
+      return Promise.reject(new Error(wiki.lang.t('auth:errors.invaliduseremail')))
+    }
 
 
-  return db.User.findOneAndUpdate({
-    email: primaryEmail,
-    provider: profile.provider
-  }, {
-    email: primaryEmail,
-    provider: profile.provider,
-    providerId: profile.id,
-    name: profile.displayName || profile.cn || _.split(primaryEmail, '@')[0]
-  }, {
-    new: true
-  }).then((user) => {
-    // Handle unregistered accounts
-    if (!user && profile.provider !== 'local' && (appconfig.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
-      let nUsr = {
-        email: primaryEmail,
-        provider: profile.provider,
-        providerId: profile.id,
-        password: '',
-        name: profile.displayName || profile.name || profile.cn,
-        rights: [{
-          role: 'read',
-          path: '/',
-          exact: false,
-          deny: false
-        }]
+    profile.provider = _.lowerCase(profile.provider)
+    primaryEmail = _.toLower(primaryEmail)
+
+    return wiki.db.User.findOneAndUpdate({
+      email: primaryEmail,
+      provider: profile.provider
+    }, {
+      email: primaryEmail,
+      provider: profile.provider,
+      providerId: profile.id,
+      name: profile.displayName || _.split(primaryEmail, '@')[0]
+    }, {
+      new: true
+    }).then((user) => {
+      // Handle unregistered accounts
+      if (!user && profile.provider !== 'local' && (appconfig.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) {
+        let nUsr = {
+          email: primaryEmail,
+          provider: profile.provider,
+          providerId: profile.id,
+          password: '',
+          name: profile.displayName || profile.name || profile.cn,
+          rights: [{
+            role: 'read',
+            path: '/',
+            exact: false,
+            deny: false
+          }]
+        }
+        return wiki.db.User.create(nUsr)
       }
       }
-      return db.User.create(nUsr)
-    }
-    return user || Promise.reject(new Error(lang.t('auth:errors:notyetauthorized')))
-  })
-}
+      return user || Promise.reject(new Error(wiki.lang.t('auth:errors:notyetauthorized')))
+    })
+  }
 
 
-userSchema.statics.hashPassword = (rawPwd) => {
-  return bcrypt.hash(rawPwd)
-}
+  userSchema.hashPassword = (rawPwd) => {
+    return bcrypt.hash(rawPwd)
+  }
 
 
-userSchema.methods.validatePassword = function (rawPwd) {
-  return bcrypt.compare(rawPwd, this.password).then((isValid) => {
-    return (isValid) ? true : Promise.reject(new Error(lang.t('auth:errors:invalidlogin')))
-  })
+  return userSchema
 }
 }
-
-module.exports = Mongoose.model('User', userSchema)

+ 106 - 0
server/modules/auth.js

@@ -0,0 +1,106 @@
+/* global wiki */
+
+const _ = require('lodash')
+const passport = require('passport')
+const fs = require('fs-extra')
+const path = require('path')
+
+module.exports = {
+  strategies: {},
+  init() {
+    this.passport = passport
+
+    // Serialization user methods
+
+    passport.serializeUser(function (user, done) {
+      done(null, user._id)
+    })
+
+    passport.deserializeUser(function (id, done) {
+      wiki.db.User.findById(id).then((user) => {
+        if (user) {
+          done(null, user)
+        } else {
+          done(new Error(wiki.lang.t('auth:errors:usernotfound')), null)
+        }
+        return true
+      }).catch((err) => {
+        done(err, null)
+      })
+    })
+
+    // Load authentication strategies
+
+    wiki.config.auth.strategies.local = {}
+
+    _.forOwn(wiki.config.auth.strategies, (strategyConfig, strategyKey) => {
+      strategyConfig.callbackURL = `${wiki.config.site.host}${wiki.config.site.path}/login/${strategyKey}/callback`
+      let strategy = require(`../authentication/${strategyKey}`)
+      strategy.init(passport, strategyConfig)
+      fs.readFile(path.join(wiki.ROOTPATH, `assets/svg/auth-icon-${strategyKey}.svg`), 'utf8').then(iconData => {
+        strategy.icon = iconData
+      }).catch(err => {
+        if (err.code === 'ENOENT') {
+          strategy.icon = '[missing icon]'
+        } else {
+          wiki.logger.error(err)
+        }
+      })
+      this.strategies[strategy.key] = strategy
+      wiki.logger.info(`Authentication Provider ${strategyKey}: OK`)
+    })
+
+    // Create Guest account for first-time
+
+    wiki.db.User.findOne({
+      where: {
+        provider: 'local',
+        email: 'guest@example.com'
+      }
+    }).then((c) => {
+      if (c < 1) {
+        return wiki.db.User.create({
+          provider: 'local',
+          email: 'guest@example.com',
+          name: 'Guest',
+          password: '',
+          role: 'guest'
+        }).then(() => {
+          wiki.logger.info('[AUTH] Guest account created successfully!')
+          return true
+        }).catch((err) => {
+          wiki.logger.error('[AUTH] An error occured while creating guest account:')
+          wiki.logger.error(err)
+          return err
+        })
+      }
+    })
+
+    // .then(() => {
+    //   if (process.env.WIKI_JS_HEROKU) {
+    //     return wiki.db.User.findOne({ provider: 'local', email: process.env.WIKI_ADMIN_EMAIL }).then((c) => {
+    //       if (c < 1) {
+    //         // Create root admin account (HEROKU ONLY)
+
+    //         return wiki.db.User.create({
+    //           provider: 'local',
+    //           email: process.env.WIKI_ADMIN_EMAIL,
+    //           name: 'Administrator',
+    //           password: '$2a$04$MAHRw785Xe/Jd5kcKzr3D.VRZDeomFZu2lius4gGpZZ9cJw7B7Mna', // admin123 (default)
+    //           role: 'admin'
+    //         }).then(() => {
+    //           wiki.logger.info('[AUTH] Root admin account created successfully!')
+    //           return true
+    //         }).catch((err) => {
+    //           wiki.logger.error('[AUTH] An error occured while creating root admin account:')
+    //           wiki.logger.error(err)
+    //           return err
+    //         })
+    //       } else { return true }
+    //     })
+    //   } else { return true }
+    // })
+
+    return this
+  }
+}

+ 85 - 0
server/modules/config.js

@@ -0,0 +1,85 @@
+/* global wiki */
+
+const fs = require('fs')
+const yaml = require('js-yaml')
+const _ = require('lodash')
+const path = require('path')
+const cfgHelper = require('../helpers/config')
+
+module.exports = {
+  /**
+   * Load root config from disk
+   */
+  init() {
+    let confPaths = {
+      config: path.join(wiki.ROOTPATH, 'config.yml'),
+      data: path.join(wiki.SERVERPATH, 'app/data.yml'),
+      dataRegex: path.join(wiki.SERVERPATH, 'app/regex.js')
+    }
+
+    let appconfig = {}
+    let appdata = {}
+
+    try {
+      appconfig = yaml.safeLoad(
+        cfgHelper.parseConfigValue(
+          fs.readFileSync(confPaths.config, 'utf8')
+        )
+      )
+      appdata = yaml.safeLoad(fs.readFileSync(confPaths.data, 'utf8'))
+      appdata.regex = require(confPaths.dataRegex)
+    } catch (ex) {
+      console.error(ex)
+      process.exit(1)
+    }
+
+    // Merge with defaults
+
+    appconfig = _.defaultsDeep(appconfig, appdata.defaults.config)
+
+    // Check port
+
+    if (appconfig.port < 1) {
+      appconfig.port = process.env.PORT || 80
+    }
+
+    // Convert booleans
+
+    appconfig.public = (appconfig.public === true || _.toLower(appconfig.public) === 'true')
+
+    // List authentication strategies
+    wiki.config = appconfig
+    wiki.data = appdata
+  },
+
+  /**
+   * Load config from DB
+   *
+   * @param {Array} subsets Array of subsets to load
+   * @returns Promise
+   */
+  loadFromDb(subsets) {
+    if (!_.isArray(subsets) || subsets.length === 0) {
+      subsets = wiki.data.configNamespaces
+    }
+
+    return wiki.db.Setting.findAll({
+      attributes: ['key', 'config'],
+      where: {
+        key: {
+          $in: subsets
+        }
+      }
+    }).then(results => {
+      if (_.isArray(results) && results.length === subsets.length) {
+        results.forEach(result => {
+          wiki.config[result.key] = result.config
+        })
+        return true
+      } else {
+        wiki.logger.warn('DB Configuration is empty or incomplete.')
+        return false
+      }
+    })
+  }
+}

+ 137 - 0
server/modules/db.js

@@ -0,0 +1,137 @@
+'use strict'
+
+/* global wiki */
+
+const fs = require('fs')
+const path = require('path')
+const _ = require('lodash')
+const Promise = require('bluebird')
+const Sequelize = require('sequelize')
+const Op = Sequelize.Op
+
+const operatorsAliases = {
+  $eq: Op.eq,
+  $ne: Op.ne,
+  $gte: Op.gte,
+  $gt: Op.gt,
+  $lte: Op.lte,
+  $lt: Op.lt,
+  $not: Op.not,
+  $in: Op.in,
+  $notIn: Op.notIn,
+  $is: Op.is,
+  $like: Op.like,
+  $notLike: Op.notLike,
+  $iLike: Op.iLike,
+  $notILike: Op.notILike,
+  $regexp: Op.regexp,
+  $notRegexp: Op.notRegexp,
+  $iRegexp: Op.iRegexp,
+  $notIRegexp: Op.notIRegexp,
+  $between: Op.between,
+  $notBetween: Op.notBetween,
+  $overlap: Op.overlap,
+  $contains: Op.contains,
+  $contained: Op.contained,
+  $adjacent: Op.adjacent,
+  $strictLeft: Op.strictLeft,
+  $strictRight: Op.strictRight,
+  $noExtendRight: Op.noExtendRight,
+  $noExtendLeft: Op.noExtendLeft,
+  $and: Op.and,
+  $or: Op.or,
+  $any: Op.any,
+  $all: Op.all,
+  $values: Op.values,
+  $col: Op.col
+}
+
+/**
+ * PostgreSQL DB module
+ */
+module.exports = {
+
+  Sequelize,
+  Op: Sequelize.Op,
+
+  /**
+   * Initialize DB
+   *
+   * @return     {Object}  DB instance
+   */
+  init() {
+    let self = this
+
+    let dbModelsPath = path.join(wiki.SERVERPATH, 'models')
+
+    // Define Sequelize instance
+
+    self.inst = new self.Sequelize(wiki.config.db.db, wiki.config.db.user, wiki.config.db.pass, {
+      host: wiki.config.db.host,
+      port: wiki.config.db.port,
+      dialect: 'postgres',
+      pool: {
+        max: 10,
+        min: 0,
+        idle: 10000
+      },
+      logging: log => { wiki.logger.log('verbose', log) },
+      operatorsAliases
+    })
+
+    // Attempt to connect and authenticate to DB
+
+    self.inst.authenticate().then(() => {
+      wiki.logger.info('Database (PostgreSQL) connection: OK')
+    }).catch(err => {
+      wiki.logger.error('Failed to connect to PostgreSQL instance.')
+      return err
+    })
+
+    // Load DB Models
+
+    fs
+      .readdirSync(dbModelsPath)
+      .filter(file => {
+        return (file.indexOf('.') !== 0 && file.indexOf('_') !== 0)
+      })
+      .forEach(file => {
+        let modelName = _.upperFirst(_.camelCase(_.split(file, '.')[0]))
+        self[modelName] = self.inst.import(path.join(dbModelsPath, file))
+      })
+
+    // Associate DB Models
+
+    require(path.join(dbModelsPath, '_relations.js'))(self)
+
+    // Set init tasks
+
+    let initTasks = {
+      // -> Sync DB Schemas
+      syncSchemas() {
+        return self.inst.sync({
+          force: false,
+          logging: log => { wiki.logger.log('verbose', log) }
+        })
+      },
+      // -> Set Connection App Name
+      setAppName() {
+        return self.inst.query(`set application_name = 'Wiki.js'`, { raw: true })
+      }
+    }
+
+    let initTasksQueue = (wiki.IS_MASTER) ? [
+      initTasks.syncSchemas,
+      initTasks.setAppName
+    ] : [
+      initTasks.setAppName
+    ]
+
+    // Perform init tasks
+
+    self.onReady = Promise.each(initTasksQueue, t => t()).return(true)
+
+    return self
+  }
+
+}

+ 27 - 35
server/libs/local.js → server/modules/disk.js

@@ -1,6 +1,6 @@
 'use strict'
 'use strict'
 
 
-/* global lang, winston */
+/* global wiki */
 
 
 const path = require('path')
 const path = require('path')
 const Promise = require('bluebird')
 const Promise = require('bluebird')
@@ -10,7 +10,7 @@ const os = require('os')
 const _ = require('lodash')
 const _ = require('lodash')
 
 
 /**
 /**
- * Local Data Storage
+ * Local Disk Storage
  */
  */
 module.exports = {
 module.exports = {
 
 
@@ -21,29 +21,26 @@ module.exports = {
 
 
   /**
   /**
    * Initialize Local Data Storage model
    * Initialize Local Data Storage model
-   *
-   * @return     {Object}  Local Data Storage model instance
    */
    */
   init () {
   init () {
-    this._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
-    this._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
+    this._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
+    this._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
 
 
-    this.createBaseDirectories(appconfig)
-    this.initMulter(appconfig)
+    this.createBaseDirectories()
+    // this.initMulter()
 
 
     return this
     return this
   },
   },
 
 
   /**
   /**
    * Init Multer upload handlers
    * Init Multer upload handlers
-   *
-   * @param      {Object}   appconfig  The application config
-   * @return     {boolean}  Void
    */
    */
-  initMulter (appconfig) {
+  initMulter () {
     let maxFileSizes = {
     let maxFileSizes = {
-      img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
-      file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
+      // img: wiki.config.uploads.maxImageFileSize * 1024 * 1024,
+      // file: wiki.config.uploads.maxOtherFileSize * 1024 * 1024
+      img: 3 * 1024 * 1024,
+      file: 10 * 1024 * 1024
     }
     }
 
 
     // -> IMAGES
     // -> IMAGES
@@ -51,7 +48,7 @@ module.exports = {
     this.uploadImgHandler = multer({
     this.uploadImgHandler = multer({
       storage: multer.diskStorage({
       storage: multer.diskStorage({
         destination: (req, f, cb) => {
         destination: (req, f, cb) => {
-          cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
+          cb(null, path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'temp-upload'))
         }
         }
       }),
       }),
       fileFilter: (req, f, cb) => {
       fileFilter: (req, f, cb) => {
@@ -76,7 +73,7 @@ module.exports = {
     this.uploadFileHandler = multer({
     this.uploadFileHandler = multer({
       storage: multer.diskStorage({
       storage: multer.diskStorage({
         destination: (req, f, cb) => {
         destination: (req, f, cb) => {
-          cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'))
+          cb(null, path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'temp-upload'))
         }
         }
       }),
       }),
       fileFilter: (req, f, cb) => {
       fileFilter: (req, f, cb) => {
@@ -95,35 +92,30 @@ module.exports = {
 
 
   /**
   /**
    * Creates a base directories (Synchronous).
    * Creates a base directories (Synchronous).
-   *
-   * @param      {Object}  appconfig  The application config
-   * @return     {Void}  Void
    */
    */
-  createBaseDirectories (appconfig) {
-    winston.info('Checking data directories...')
-
+  createBaseDirectories () {
     try {
     try {
-      fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data))
-      fs.emptyDirSync(path.resolve(ROOTPATH, appconfig.paths.data))
-      fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './cache'))
-      fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs'))
-      fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'))
+      fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data))
+      fs.emptyDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data))
+      fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './cache'))
+      fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './thumbs'))
+      fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './temp-upload'))
 
 
       if (os.type() !== 'Windows_NT') {
       if (os.type() !== 'Windows_NT') {
-        fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '755')
+        fs.chmodSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.data, './temp-upload'), '755')
       }
       }
 
 
-      fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo))
-      fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'))
+      fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo))
+      fs.ensureDirSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, './uploads'))
 
 
       if (os.type() !== 'Windows_NT') {
       if (os.type() !== 'Windows_NT') {
-        fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'), '755')
+        fs.chmodSync(path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, './uploads'), '755')
       }
       }
     } catch (err) {
     } catch (err) {
-      winston.error(err)
+      wiki.logger.error(err)
     }
     }
 
 
-    winston.info('Data and Repository directories are OK.')
+    wiki.logger.info('Disk Data Paths: OK')
   },
   },
 
 
   /**
   /**
@@ -154,7 +146,7 @@ module.exports = {
    */
    */
   validateUploadsFilename (f, fld, isImage) {
   validateUploadsFilename (f, fld, isImage) {
     let fObj = path.parse(f)
     let fObj = path.parse(f)
-    let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(new RegExp('[^a-z0-9-' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g'), '')
+    let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(new RegExp('[^a-z0-9-' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']', 'g'), '')
     let fext = _.toLower(fObj.ext)
     let fext = _.toLower(fObj.ext)
 
 
     if (isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
     if (isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
@@ -165,7 +157,7 @@ module.exports = {
     let fpath = path.resolve(this._uploadsPath, fld, f)
     let fpath = path.resolve(this._uploadsPath, fld, f)
 
 
     return fs.statAsync(fpath).then((s) => {
     return fs.statAsync(fpath).then((s) => {
-      throw new Error(lang.t('errors:fileexists', { path: f }))
+      throw new Error(wiki.lang.t('errors:fileexists', { path: f }))
     }).catch((err) => {
     }).catch((err) => {
       if (err.code === 'ENOENT') {
       if (err.code === 'ENOENT') {
         return f
         return f

+ 6 - 6
server/libs/entries.js → server/modules/documents.js

@@ -1,6 +1,6 @@
 'use strict'
 'use strict'
 
 
-/* global db, git, lang, mark, rights, search, winston */
+/* global wiki */
 
 
 const Promise = require('bluebird')
 const Promise = require('bluebird')
 const path = require('path')
 const path = require('path')
@@ -10,7 +10,7 @@ const _ = require('lodash')
 const entryHelper = require('../helpers/entry')
 const entryHelper = require('../helpers/entry')
 
 
 /**
 /**
- * Entries Model
+ * Documents Model
  */
  */
 module.exports = {
 module.exports = {
 
 
@@ -25,10 +25,10 @@ module.exports = {
   init() {
   init() {
     let self = this
     let self = this
 
 
-    self._repoPath = path.resolve(ROOTPATH, appconfig.paths.repo)
-    self._cachePath = path.resolve(ROOTPATH, appconfig.paths.data, 'cache')
-    appdata.repoPath = self._repoPath
-    appdata.cachePath = self._cachePath
+    self._repoPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo)
+    self._cachePath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'cache')
+    wiki.data.repoPath = self._repoPath
+    wiki.data.cachePath = self._cachePath
 
 
     return self
     return self
   },
   },

+ 32 - 59
server/libs/git.js → server/modules/git.js

@@ -1,6 +1,6 @@
 'use strict'
 'use strict'
 
 
-/* global lang, winston */
+/* global wiki */
 
 
 const Git = require('git-wrapper2-promise')
 const Git = require('git-wrapper2-promise')
 const Promise = require('bluebird')
 const Promise = require('bluebird')
@@ -43,21 +43,19 @@ module.exports = {
 
 
     // -> Build repository path
     // -> Build repository path
 
 
-    if (_.isEmpty(appconfig.paths.repo)) {
-      self._repo.path = path.join(ROOTPATH, 'repo')
+    if (_.isEmpty(wiki.config.paths.repo)) {
+      self._repo.path = path.join(wiki.ROOTPATH, 'repo')
     } else {
     } else {
-      self._repo.path = appconfig.paths.repo
+      self._repo.path = wiki.config.paths.repo
     }
     }
 
 
     // -> Initialize repository
     // -> Initialize repository
 
 
-    self.onReady = self._initRepo(appconfig)
+    self.onReady = (wiki.IS_MASTER) ? self._initRepo() : Promise.resolve()
 
 
-    if (appconfig.git) {
-      // Set repo branch
-      self._repo.branch = appconfig.git.branch || 'master'
-      // Define signature
-      self._signature.email = appconfig.git.serverEmail || 'wiki@example.com'
+    if (wiki.config.git) {
+      self._repo.branch = wiki.config.git.branch || 'master'
+      self._signature.email = wiki.config.git.serverEmail || 'wiki@example.com'
     }
     }
 
 
     return self
     return self
@@ -66,19 +64,17 @@ module.exports = {
   /**
   /**
    * Initialize Git repository
    * Initialize Git repository
    *
    *
-   * @param      {Object}  appconfig  The application config
+   * @param      {Object}  wiki.config  The application config
    * @return     {Object}  Promise
    * @return     {Object}  Promise
    */
    */
-  _initRepo(appconfig) {
+  _initRepo() {
     let self = this
     let self = this
 
 
-    winston.info('Checking Git repository...')
-
     // -> Check if path is accessible
     // -> Check if path is accessible
 
 
     return fs.mkdirAsync(self._repo.path).catch((err) => {
     return fs.mkdirAsync(self._repo.path).catch((err) => {
       if (err.code !== 'EEXIST') {
       if (err.code !== 'EEXIST') {
-        winston.error('Invalid Git repository path or missing permissions.')
+        wiki.logger.error('Invalid Git repository path or missing permissions.')
       }
       }
     }).then(() => {
     }).then(() => {
       self._git = new Git({ 'git-dir': self._repo.path })
       self._git = new Git({ 'git-dir': self._repo.path })
@@ -92,28 +88,28 @@ module.exports = {
         self._repo.exists = false
         self._repo.exists = false
       })
       })
     }).then(() => {
     }).then(() => {
-      if (appconfig.git === false) {
-        winston.info('Remote Git syncing is disabled. Not recommended!')
+      if (wiki.config.git === false) {
+        wiki.logger.warn('Remote Git syncing is disabled. Not recommended!')
         return Promise.resolve(true)
         return Promise.resolve(true)
       }
       }
 
 
       // Initialize remote
       // Initialize remote
 
 
-      let urlObj = URL.parse(appconfig.git.url)
-      if (appconfig.git.auth.type !== 'ssh') {
-        urlObj.auth = appconfig.git.auth.username + ':' + appconfig.git.auth.password
+      let urlObj = URL.parse(wiki.config.git.url)
+      if (wiki.config.git.auth.type !== 'ssh') {
+        urlObj.auth = wiki.config.git.auth.username + ':' + wiki.config.git.auth.password
       }
       }
       self._url = URL.format(urlObj)
       self._url = URL.format(urlObj)
 
 
       let gitConfigs = [
       let gitConfigs = [
         () => { return self._git.exec('config', ['--local', 'user.name', 'Wiki']) },
         () => { return self._git.exec('config', ['--local', 'user.name', 'Wiki']) },
         () => { return self._git.exec('config', ['--local', 'user.email', self._signature.email]) },
         () => { return self._git.exec('config', ['--local', 'user.email', self._signature.email]) },
-        () => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(appconfig.git.auth.sslVerify)]) }
+        () => { return self._git.exec('config', ['--local', '--bool', 'http.sslVerify', _.toString(wiki.config.git.auth.sslVerify)]) }
       ]
       ]
 
 
-      if (appconfig.git.auth.type === 'ssh') {
+      if (wiki.config.git.auth.type === 'ssh') {
         gitConfigs.push(() => {
         gitConfigs.push(() => {
-          return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + appconfig.git.auth.privateKey + '" -o StrictHostKeyChecking=no'])
+          return self._git.exec('config', ['--local', 'core.sshCommand', 'ssh -i "' + wiki.config.git.auth.privateKey + '" -o StrictHostKeyChecking=no'])
         })
         })
       }
       }
 
 
@@ -126,14 +122,14 @@ module.exports = {
             return self._git.exec('remote', ['set-url', 'origin', self._url])
             return self._git.exec('remote', ['set-url', 'origin', self._url])
           }
           }
         }).catch(err => {
         }).catch(err => {
-          winston.error(err)
+          wiki.logger.error(err)
         })
         })
       })
       })
     }).catch((err) => {
     }).catch((err) => {
-      winston.error('Git remote error!')
+      wiki.logger.error('Git remote error!')
       throw err
       throw err
     }).then(() => {
     }).then(() => {
-      winston.info('Git repository is OK.')
+      wiki.logger.info('Git Repository: OK')
       return true
       return true
     })
     })
   },
   },
@@ -144,7 +140,7 @@ module.exports = {
    * @return     {String}  The repo path.
    * @return     {String}  The repo path.
    */
    */
   getRepoPath() {
   getRepoPath() {
-    return this._repo.path || path.join(ROOTPATH, 'repo')
+    return this._repo.path || path.join(wiki.ROOTPATH, 'repo')
   },
   },
 
 
   /**
   /**
@@ -157,18 +153,18 @@ module.exports = {
 
 
     // Is git remote disabled?
     // Is git remote disabled?
 
 
-    if (appconfig.git === false) {
+    if (wiki.config.git === false) {
       return Promise.resolve(true)
       return Promise.resolve(true)
     }
     }
 
 
     // Fetch
     // Fetch
 
 
-    winston.info('Performing pull from remote Git repository...')
+    wiki.logger.info('Performing pull from remote Git repository...')
     return self._git.pull('origin', self._repo.branch).then((cProc) => {
     return self._git.pull('origin', self._repo.branch).then((cProc) => {
-      winston.info('Git Pull completed.')
+      wiki.logger.info('Git Pull completed.')
     })
     })
       .catch((err) => {
       .catch((err) => {
-        winston.error('Unable to fetch from git origin!')
+        wiki.logger.error('Unable to fetch from git origin!')
         throw err
         throw err
       })
       })
       .then(() => {
       .then(() => {
@@ -178,19 +174,19 @@ module.exports = {
           let out = cProc.stdout.toString()
           let out = cProc.stdout.toString()
 
 
           if (_.includes(out, 'commit')) {
           if (_.includes(out, 'commit')) {
-            winston.info('Performing push to remote Git repository...')
+            wiki.logger.info('Performing push to remote Git repository...')
             return self._git.push('origin', self._repo.branch).then(() => {
             return self._git.push('origin', self._repo.branch).then(() => {
-              return winston.info('Git Push completed.')
+              return wiki.logger.info('Git Push completed.')
             })
             })
           } else {
           } else {
-            winston.info('Git Push skipped. Repository is already in sync.')
+            wiki.logger.info('Git Push skipped. Repository is already in sync.')
           }
           }
 
 
           return true
           return true
         })
         })
       })
       })
       .catch((err) => {
       .catch((err) => {
-        winston.error('Unable to push changes to remote Git repository!')
+        wiki.logger.error('Unable to push changes to remote Git repository!')
         throw err
         throw err
       })
       })
   },
   },
@@ -210,7 +206,7 @@ module.exports = {
       let out = cProc.stdout.toString()
       let out = cProc.stdout.toString()
       return _.includes(out, gitFilePath)
       return _.includes(out, gitFilePath)
     }).then((isTracked) => {
     }).then((isTracked) => {
-      commitMsg = (isTracked) ? lang.t('git:updated', { path: gitFilePath }) : lang.t('git:added', { path: gitFilePath })
+      commitMsg = (isTracked) ? wiki.lang.t('git:updated', { path: gitFilePath }) : wiki.lang.t('git:added', { path: gitFilePath })
       return self._git.add(gitFilePath)
       return self._git.add(gitFilePath)
     }).then(() => {
     }).then(() => {
       let commitUsr = securityHelper.sanitizeCommitUser(author)
       let commitUsr = securityHelper.sanitizeCommitUser(author)
@@ -245,29 +241,6 @@ module.exports = {
     })
     })
   },
   },
 
 
-  /**
-   * Delete a document.
-   *
-   * @param      {String}            entryPath     The entry path
-   * @return     {Promise<Boolean>}  Resolve on success
-   */
-  deleteDocument(entryPath, author) {
-    let self = this
-    let gitFilePath = entryPath + '.md'
-
-    return this._git.exec('rm', [gitFilePath]).then((cProc) => {
-      let out = cProc.stdout.toString()
-      if (_.includes(out, 'fatal')) {
-        let errorMsg = _.capitalize(_.head(_.split(_.replace(out, 'fatal: ', ''), ',')))
-        throw new Error(errorMsg)
-      }
-      let commitUsr = securityHelper.sanitizeCommitUser(author)
-      return self._git.exec('commit', ['-m', lang.t('git:deleted', { path: gitFilePath }), '--author="' + commitUsr.name + ' <' + commitUsr.email + '>"']).catch((err) => {
-        if (_.includes(err.stdout, 'nothing to commit')) { return true }
-      })
-    })
-  },
-
   /**
   /**
    * Commits uploads changes.
    * Commits uploads changes.
    *
    *

+ 43 - 0
server/modules/graphql.js

@@ -0,0 +1,43 @@
+'use strict'
+
+/* global wiki */
+
+const gqlTools = require('graphql-tools')
+const fs = require('fs')
+const path = require('path')
+const _ = require('lodash')
+
+const typeDefs = fs.readFileSync(path.join(wiki.SERVERPATH, 'schemas/types.graphql'), 'utf8')
+
+const DateScalar = require('../schemas/scalar-date')
+const AuthenticationResolvers = require('../schemas/resolvers-authentication')
+const CommentResolvers = require('../schemas/resolvers-comment')
+const DocumentResolvers = require('../schemas/resolvers-document')
+const FileResolvers = require('../schemas/resolvers-file')
+const FolderResolvers = require('../schemas/resolvers-folder')
+const GroupResolvers = require('../schemas/resolvers-group')
+const SettingResolvers = require('../schemas/resolvers-setting')
+const TagResolvers = require('../schemas/resolvers-tag')
+const TranslationResolvers = require('../schemas/resolvers-translation')
+const UserResolvers = require('../schemas/resolvers-user')
+
+const resolvers = _.merge(
+  AuthenticationResolvers,
+  CommentResolvers,
+  DocumentResolvers,
+  FileResolvers,
+  FolderResolvers,
+  GroupResolvers,
+  SettingResolvers,
+  TagResolvers,
+  TranslationResolvers,
+  UserResolvers,
+  DateScalar
+)
+
+const Schema = gqlTools.makeExecutableSchema({
+  typeDefs,
+  resolvers
+})
+
+module.exports = Schema

+ 91 - 0
server/modules/kernel.js

@@ -0,0 +1,91 @@
+const cluster = require('cluster')
+const Promise = require('bluebird')
+const _ = require('lodash')
+
+/* global wiki */
+
+module.exports = {
+  numWorkers: 1,
+  workers: [],
+  init() {
+    if (cluster.isMaster) {
+      wiki.logger.info('=======================================')
+      wiki.logger.info('= Wiki.js =============================')
+      wiki.logger.info('=======================================')
+
+      wiki.redis = require('./redis').init()
+      wiki.queue = require('./queue').init()
+
+      this.setWorkerLimit()
+      this.bootMaster()
+    } else {
+      this.bootWorker()
+    }
+  },
+  /**
+   * Pre-Master Boot Sequence
+   */
+  preBootMaster() {
+    return Promise.mapSeries([
+      () => { return wiki.db.onReady },
+      () => { return wiki.configSvc.loadFromDb() },
+      () => { return wiki.queue.clean() }
+    ], fn => { return fn() })
+  },
+  /**
+   * Boot Master Process
+   */
+  bootMaster() {
+    this.preBootMaster().then(sequenceResults => {
+      if (_.every(sequenceResults, rs => rs === true)) {
+        this.postBootMaster()
+      } else {
+        wiki.logger.info('Starting configuration manager...')
+        require('../configure')()
+      }
+      return true
+    }).catch(err => {
+      wiki.logger.error(err)
+      process.exit(1)
+    })
+  },
+  /**
+   * Post-Master Boot Sequence
+   */
+  postBootMaster() {
+    require('../master')().then(() => {
+      _.times(this.numWorker, this.spawnWorker)
+
+      wiki.queue.uplClearTemp.add({}, {
+        repeat: { cron: '*/15 * * * *' }
+      })
+    })
+
+    cluster.on('exit', (worker, code, signal) => {
+      wiki.logger.info(`Background Worker #${worker.id} was terminated.`)
+    })
+  },
+  /**
+   * Boot Worker Process
+   */
+  bootWorker() {
+    wiki.logger.info(`Background Worker #${cluster.worker.id} is initializing...`)
+    require('../worker')
+  },
+  /**
+   * Spawn new Worker process
+   */
+  spawnWorker() {
+    this.workers.push(cluster.fork())
+  },
+  /**
+   * Set Worker count based on config + system capabilities
+   */
+  setWorkerLimit() {
+    const numCPUs = require('os').cpus().length
+    this.numWorkers = (wiki.config.workers > 0) ? wiki.config.workers : numCPUs
+    if (this.numWorkers > numCPUs) {
+      this.numWorkers = numCPUs
+    }
+  }
+}

+ 52 - 0
server/modules/localization.js

@@ -0,0 +1,52 @@
+const _ = require('lodash')
+const dotize = require('dotize')
+const i18nBackend = require('i18next-node-fs-backend')
+const i18next = require('i18next')
+const path = require('path')
+const Promise = require('bluebird')
+
+/* global wiki */
+
+module.exports = {
+  engine: null,
+  namespaces: ['common', 'admin', 'auth', 'errors', 'git'],
+  init() {
+    this.engine = i18next
+    this.engine.use(i18nBackend).init({
+      load: 'languageOnly',
+      ns: this.namespaces,
+      defaultNS: 'common',
+      saveMissing: false,
+      preload: [wiki.config.site.lang],
+      lng: wiki.config.site.lang,
+      fallbackLng: 'en',
+      backend: {
+        loadPath: path.join(wiki.SERVERPATH, 'locales/{{lng}}/{{ns}}.json')
+      }
+    })
+    return this
+  },
+  getByNamespace(locale, namespace) {
+    if (this.engine.hasResourceBundle(locale, namespace)) {
+      let data = this.engine.getResourceBundle(locale, namespace)
+      return _.map(dotize.convert(data), (value, key) => {
+        return {
+          key,
+          value
+        }
+      })
+    } else {
+      throw new Error('Invalid locale or namespace')
+    }
+  },
+  loadLocale(locale) {
+    return Promise.fromCallback(cb => {
+      return this.engine.loadLanguages(locale, cb)
+    })
+  },
+  setCurrentLocale(locale) {
+    return Promise.fromCallback(cb => {
+      return this.engine.changeLanguage(locale, cb)
+    })
+  }
+}

+ 80 - 0
server/modules/logger.js

@@ -0,0 +1,80 @@
+'use strict'
+
+/* global wiki */
+
+const cluster = require('cluster')
+
+module.exports = {
+  init() {
+    let winston = require('winston')
+
+    // Console
+
+    let logger = new (winston.Logger)({
+      level: (wiki.IS_DEBUG) ? 'debug' : 'info',
+      transports: [
+        new (winston.transports.Console)({
+          level: (wiki.IS_DEBUG) ? 'debug' : 'info',
+          prettyPrint: true,
+          colorize: true,
+          silent: false,
+          timestamp: true
+        })
+      ]
+    })
+
+    logger.filters.push((level, msg) => {
+      let processName = (cluster.isMaster) ? 'MASTER' : `WORKER-${cluster.worker.id}`
+      return '[' + processName + '] ' + msg
+    })
+
+    // External services
+
+    // if (wiki.config.externalLogging.bugsnag) {
+    //   const bugsnagTransport = require('./winston-transports/bugsnag')
+    //   logger.add(bugsnagTransport, {
+    //     level: 'warn',
+    //     key: wiki.config.externalLogging.bugsnag
+    //   })
+    // }
+
+    // if (wiki.config.externalLogging.loggly) {
+    //   require('winston-loggly-bulk')
+    //   logger.add(winston.transports.Loggly, {
+    //     token: wiki.config.externalLogging.loggly.token,
+    //     subdomain: wiki.config.externalLogging.loggly.subdomain,
+    //     tags: ['wiki-js'],
+    //     level: 'warn',
+    //     json: true
+    //   })
+    // }
+
+    // if (wiki.config.externalLogging.papertrail) {
+    //   require('winston-papertrail').Papertrail // eslint-disable-line no-unused-expressions
+    //   logger.add(winston.transports.Papertrail, {
+    //     host: wiki.config.externalLogging.papertrail.host,
+    //     port: wiki.config.externalLogging.papertrail.port,
+    //     level: 'warn',
+    //     program: 'wiki.js'
+    //   })
+    // }
+
+    // if (wiki.config.externalLogging.rollbar) {
+    //   const rollbarTransport = require('./winston-transports/rollbar')
+    //   logger.add(rollbarTransport, {
+    //     level: 'warn',
+    //     key: wiki.config.externalLogging.rollbar
+    //   })
+    // }
+
+    // if (wiki.config.externalLogging.sentry) {
+    //   const sentryTransport = require('./winston-transports/sentry')
+    //   logger.add(sentryTransport, {
+    //     level: 'warn',
+    //     key: wiki.config.externalLogging.sentry
+    //   })
+    // }
+
+    return logger
+  }
+}

+ 9 - 7
server/libs/markdown.js → server/modules/markdown.js

@@ -1,6 +1,6 @@
 'use strict'
 'use strict'
 
 
-/* global winston */
+/* global wiki */
 
 
 const Promise = require('bluebird')
 const Promise = require('bluebird')
 const md = require('markdown-it')
 const md = require('markdown-it')
@@ -23,11 +23,12 @@ const mdRemove = require('remove-markdown')
 
 
 var mkdown = md({
 var mkdown = md({
   html: true,
   html: true,
-  breaks: appconfig.features.linebreaks,
+  // breaks: wiki.config.features.linebreaks,
+  breaks: true,
   linkify: true,
   linkify: true,
   typography: true,
   typography: true,
   highlight(str, lang) {
   highlight(str, lang) {
-    if (appconfig.theme.code.colorize && lang && hljs.getLanguage(lang)) {
+    if (wiki.config.theme.code.colorize && lang && hljs.getLanguage(lang)) {
       try {
       try {
         return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'
         return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>'
       } catch (err) {
       } catch (err) {
@@ -57,7 +58,8 @@ var mkdown = md({
   })
   })
   .use(mdAttrs)
   .use(mdAttrs)
 
 
-if (appconfig.features.mathjax) {
+// if (wiki.config.features.mathjax) {
+if (true) {
   mkdown.use(mdMathjax)
   mkdown.use(mdMathjax)
 }
 }
 
 
@@ -94,7 +96,7 @@ const videoRules = [
 
 
 // Regex
 // Regex
 
 
-const textRegex = new RegExp('\\b[a-z0-9-.,' + appdata.regex.cjk + appdata.regex.arabic + ']+\\b', 'g')
+const textRegex = new RegExp('\\b[a-z0-9-.,' + wiki.data.regex.cjk + wiki.data.regex.arabic + ']+\\b', 'g')
 const mathRegex = [
 const mathRegex = [
   {
   {
     format: 'TeX',
     format: 'TeX',
@@ -301,7 +303,7 @@ const parseContent = (content) => {
 
 
   // Mathjax Post-processor
   // Mathjax Post-processor
 
 
-  if (appconfig.features.mathjax) {
+  if (wiki.config.features.mathjax) {
     return processMathjax(cr.html())
     return processMathjax(cr.html())
   } else {
   } else {
     return Promise.resolve(cr.html())
     return Promise.resolve(cr.html())
@@ -339,7 +341,7 @@ const processMathjax = (content) => {
                 resolve(result.svg)
                 resolve(result.svg)
               } else {
               } else {
                 resolve(currentMatch[0])
                 resolve(currentMatch[0])
-                winston.warn(result.errors.join(', '))
+                wiki.logger.warn(result.errors.join(', '))
               }
               }
             })
             })
           })
           })

+ 37 - 0
server/modules/queue.js

@@ -0,0 +1,37 @@
+'use strict'
+
+/* global wiki */
+
+const Bull = require('bull')
+const Promise = require('bluebird')
+
+module.exports = {
+  init() {
+    wiki.data.queues.forEach(queueName => {
+      this[queueName] = new Bull(queueName, {
+        prefix: `q-${wiki.config.ha.nodeuid}`,
+        redis: wiki.config.redis
+      })
+    })
+    return this
+  },
+  clean() {
+    return Promise.each(wiki.data.queues, queueName => {
+      return new Promise((resolve, reject) => {
+        let keyStream = wiki.redis.scanStream({
+          match: `q-${wiki.config.ha.nodeuid}:${queueName}:*`
+        })
+        keyStream.on('data', resultKeys => {
+          if (resultKeys.length > 0) {
+            wiki.redis.del(resultKeys)
+          }
+        })
+        keyStream.on('end', resolve)
+      })
+    }).then(() => {
+      wiki.logger.info('Purging old queue jobs: OK')
+    }).return(true).catch(err => {
+      wiki.logger.error(err)
+    })
+  }
+}

+ 33 - 0
server/modules/redis.js

@@ -0,0 +1,33 @@
+'use strict'
+
+/* global wiki */
+
+const Redis = require('ioredis')
+const { isPlainObject } = require('lodash')
+
+/**
+ * Redis module
+ *
+ * @return     {Object}  Redis client wrapper instance
+ */
+module.exports = {
+
+  /**
+   * Initialize Redis client
+   *
+   * @return     {Object}  Redis client instance
+   */
+  init() {
+    if (isPlainObject(wiki.config.redis)) {
+      let red = new Redis(wiki.config.redis)
+      red.on('ready', () => {
+        wiki.logger.info('Redis connection: OK')
+      })
+      return red
+    } else {
+      wiki.logger.error('Invalid Redis configuration!')
+      process.exit(1)
+    }
+  }
+
+}

+ 3 - 3
server/libs/rights.js → server/modules/rights.js

@@ -1,6 +1,6 @@
 'use strict'
 'use strict'
 
 
-/* global db */
+/* global wiki */
 
 
 const _ = require('lodash')
 const _ = require('lodash')
 
 
@@ -32,8 +32,8 @@ module.exports = {
   init () {
   init () {
     let self = this
     let self = this
 
 
-    db.onReady.then(() => {
-      db.User.findOne({ provider: 'local', email: 'guest' }).then((u) => {
+    wiki.db.onReady.then(() => {
+      wiki.db.User.findOne({ provider: 'local', email: 'guest' }).then((u) => {
         if (u) {
         if (u) {
           self.guest = u
           self.guest = u
         }
         }

+ 13 - 13
server/libs/search.js → server/modules/search.js

@@ -1,13 +1,13 @@
 'use strict'
 'use strict'
 
 
-/* global winston */
+/* global wiki */
 
 
 const Promise = require('bluebird')
 const Promise = require('bluebird')
 const _ = require('lodash')
 const _ = require('lodash')
-const searchIndex = require('./search-index')
+// const searchIndex = require('./search-index')
 const stopWord = require('stopword')
 const stopWord = require('stopword')
 const streamToPromise = require('stream-to-promise')
 const streamToPromise = require('stream-to-promise')
-const searchAllowedChars = new RegExp('[^a-z0-9' + appdata.regex.cjk + appdata.regex.arabic + ' ]', 'g')
+const searchAllowedChars = new RegExp('[^a-z0-9' + wiki.data.regex.cjk + wiki.data.regex.arabic + ' ]', 'g')
 
 
 module.exports = {
 module.exports = {
 
 
@@ -22,24 +22,24 @@ module.exports = {
   init () {
   init () {
     let self = this
     let self = this
     self._isReady = new Promise((resolve, reject) => {
     self._isReady = new Promise((resolve, reject) => {
-      searchIndex({
+      /*searchIndex({
         deletable: true,
         deletable: true,
         fieldedSearch: true,
         fieldedSearch: true,
         indexPath: 'wiki',
         indexPath: 'wiki',
         logLevel: 'error',
         logLevel: 'error',
-        stopwords: _.get(stopWord, appconfig.lang, [])
+        stopwords: _.get(stopWord, wiki.config.lang, [])
       }, (err, si) => {
       }, (err, si) => {
         if (err) {
         if (err) {
-          winston.error('Failed to initialize search index.', err)
+          wiki.logger.error('Failed to initialize search index.', err)
           reject(err)
           reject(err)
         } else {
         } else {
           self._si = Promise.promisifyAll(si)
           self._si = Promise.promisifyAll(si)
           self._si.flushAsync().then(() => {
           self._si.flushAsync().then(() => {
-            winston.info('Search index flushed and ready.')
+            wiki.logger.info('Search index flushed and ready.')
             resolve(true)
             resolve(true)
           })
           })
         }
         }
-      })
+      }) */
     })
     })
 
 
     return self
     return self
@@ -95,13 +95,13 @@ module.exports = {
           parent: content.parent || '',
           parent: content.parent || '',
           content: content.text || ''
           content: content.text || ''
         }]).then(() => {
         }]).then(() => {
-          winston.log('verbose', 'Entry ' + content._id + ' added/updated to search index.')
+          wiki.logger.log('verbose', 'Entry ' + content._id + ' added/updated to search index.')
           return true
           return true
         }).catch((err) => {
         }).catch((err) => {
-          winston.error(err)
+          wiki.logger.error(err)
         })
         })
       }).catch((err) => {
       }).catch((err) => {
-        winston.error(err)
+        wiki.logger.error(err)
       })
       })
     })
     })
   },
   },
@@ -131,7 +131,7 @@ module.exports = {
         if (err.type === 'NotFoundError') {
         if (err.type === 'NotFoundError') {
           return true
           return true
         } else {
         } else {
-          winston.error(err)
+          wiki.logger.error(err)
         }
         }
       })
       })
     })
     })
@@ -204,7 +204,7 @@ module.exports = {
           suggest: []
           suggest: []
         }
         }
       } else {
       } else {
-        winston.error(err)
+        wiki.logger.error(err)
       }
       }
     })
     })
   }
   }

+ 0 - 0
server/libs/system.js → server/modules/system.js


+ 12 - 12
server/libs/uploads-agent.js → server/modules/uploads-agent.js

@@ -1,6 +1,6 @@
 'use strict'
 'use strict'
 
 
-/* global db, git, lang, upl */
+/* global wiki */
 
 
 const path = require('path')
 const path = require('path')
 const Promise = require('bluebird')
 const Promise = require('bluebird')
@@ -32,8 +32,8 @@ module.exports = {
   init () {
   init () {
     let self = this
     let self = this
 
 
-    self._uploadsPath = path.resolve(ROOTPATH, appconfig.paths.repo, 'uploads')
-    self._uploadsThumbsPath = path.resolve(ROOTPATH, appconfig.paths.data, 'thumbs')
+    self._uploadsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.repo, 'uploads')
+    self._uploadsThumbsPath = path.resolve(wiki.ROOTPATH, wiki.config.paths.data, 'thumbs')
 
 
     return self
     return self
   },
   },
@@ -59,16 +59,16 @@ module.exports = {
     self._watcher.on('add', (p) => {
     self._watcher.on('add', (p) => {
       let pInfo = self.parseUploadsRelPath(p)
       let pInfo = self.parseUploadsRelPath(p)
       return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
       return self.processFile(pInfo.folder, pInfo.filename).then((mData) => {
-        return db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
+        return wiki.db.UplFile.findByIdAndUpdate(mData._id, mData, { upsert: true })
       }).then(() => {
       }).then(() => {
-        return git.commitUploads(lang.t('git:uploaded', { path: p }))
+        return wiki.git.commitUploads(wiki.lang.t('git:uploaded', { path: p }))
       })
       })
     })
     })
 
 
     // -> Remove upload file
     // -> Remove upload file
 
 
     self._watcher.on('unlink', (p) => {
     self._watcher.on('unlink', (p) => {
-      return git.commitUploads(lang.t('git:deleted', { path: p }))
+      return wiki.git.commitUploads(wiki.lang.t('git:deleted', { path: p }))
     })
     })
   },
   },
 
 
@@ -91,8 +91,8 @@ module.exports = {
 
 
         // Add folders to DB
         // Add folders to DB
 
 
-        return db.UplFolder.remove({}).then(() => {
-          return db.UplFolder.insertMany(_.map(folderNames, (f) => {
+        return wiki.db.UplFolder.remove({}).then(() => {
+          return wiki.db.UplFolder.insertMany(_.map(folderNames, (f) => {
             return {
             return {
               _id: 'f:' + f,
               _id: 'f:' + f,
               name: f
               name: f
@@ -107,7 +107,7 @@ module.exports = {
             let fldPath = path.join(self._uploadsPath, fldName)
             let fldPath = path.join(self._uploadsPath, fldName)
             return fs.readdirAsync(fldPath).then((fList) => {
             return fs.readdirAsync(fldPath).then((fList) => {
               return Promise.map(fList, (f) => {
               return Promise.map(fList, (f) => {
-                return upl.processFile(fldName, f).then((mData) => {
+                return wiki.upl.processFile(fldName, f).then((mData) => {
                   if (mData) {
                   if (mData) {
                     allFiles.push(mData)
                     allFiles.push(mData)
                   }
                   }
@@ -118,9 +118,9 @@ module.exports = {
           }, {concurrency: 1}).finally(() => {
           }, {concurrency: 1}).finally(() => {
             // Add files to DB
             // Add files to DB
 
 
-            return db.UplFile.remove({}).then(() => {
+            return wiki.db.UplFile.remove({}).then(() => {
               if (_.isArray(allFiles) && allFiles.length > 0) {
               if (_.isArray(allFiles) && allFiles.length > 0) {
-                return db.UplFile.insertMany(allFiles)
+                return wiki.db.UplFile.insertMany(allFiles)
               } else {
               } else {
                 return true
                 return true
               }
               }
@@ -131,7 +131,7 @@ module.exports = {
     }).then(() => {
     }).then(() => {
       // Watch for new changes
       // Watch for new changes
 
 
-      return upl.watch()
+      return wiki.upl.watch()
     })
     })
   },
   },
 
 

部分文件因为文件数量过多而无法显示