setup.js 12.5 KB
Newer Older
1
const path = require('path')
2
const uuid = require('uuid/v4')
3 4 5 6 7 8 9 10 11 12 13 14 15
const bodyParser = require('body-parser')
const compression = require('compression')
const express = require('express')
const favicon = require('serve-favicon')
const http = require('http')
const https = require('https')
const Promise = require('bluebird')
const fs = require('fs-extra')
const _ = require('lodash')
const cfgHelper = require('./helpers/config')
const crypto = Promise.promisifyAll(require('crypto'))
const pem2jwk = require('pem-jwk').pem2jwk
const semver = require('semver')
16

17
/* global WIKI */
18

19
module.exports = () => {
20
  WIKI.config.site = {
21
    path: '',
22
    title: 'Wiki.js'
23 24
  }

25
  WIKI.system = require('./core/system')
26

27 28 29 30
  // ----------------------------------------
  // Define Express App
  // ----------------------------------------

31
  let app = express()
32 33 34 35 36 37
  app.use(compression())

  // ----------------------------------------
  // Public Assets
  // ----------------------------------------

38 39
  app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico')))
  app.use(express.static(path.join(WIKI.ROOTPATH, 'assets')))
40 41 42 43 44

  // ----------------------------------------
  // View Engine Setup
  // ----------------------------------------

45
  app.set('views', path.join(WIKI.SERVERPATH, 'views'))
46 47 48 49 50
  app.set('view engine', 'pug')

  app.use(bodyParser.json())
  app.use(bodyParser.urlencoded({ extended: false }))

51 52
  app.locals.config = WIKI.config
  app.locals.data = WIKI.data
53 54
  app.locals._ = require('lodash')

NGPixel's avatar
NGPixel committed
55 56 57 58 59
  // ----------------------------------------
  // HMR (Dev Mode Only)
  // ----------------------------------------

  if (global.DEV) {
60 61
    app.use(global.WP_DEV.devMiddleware)
    app.use(global.WP_DEV.hotMiddleware)
NGPixel's avatar
NGPixel committed
62 63
  }

64 65 66 67
  // ----------------------------------------
  // Controllers
  // ----------------------------------------

68
  app.get('*', async (req, res) => {
69
    let packageObj = await fs.readJson(path.join(WIKI.ROOTPATH, 'package.json'))
Nick's avatar
Nick committed
70
    res.render('setup', { packageObj })
71 72
  })

NGPixel's avatar
NGPixel committed
73
  /**
74
   * Finalize
NGPixel's avatar
NGPixel committed
75
   */
76 77
  app.post('/finalize', async (req, res) => {
    try {
78
      // Set config
79 80 81 82 83
      _.set(WIKI.config, 'auth', {
        audience: 'urn:wiki.js',
        tokenExpiration: '30m',
        tokenRenewal: '14d'
      })
84 85 86 87 88 89
      _.set(WIKI.config, 'company', '')
      _.set(WIKI.config, 'features', {
        featurePageRatings: true,
        featurePageComments: true,
        featurePersonalWikis: true
      })
90
      _.set(WIKI.config, 'graphEndpoint', 'https://graph.requarks.io')
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
      _.set(WIKI.config, 'host', 'http://')
      _.set(WIKI.config, 'lang', {
        code: 'en',
        autoUpdate: true,
        namespacing: false,
        namespaces: []
      })
      _.set(WIKI.config, 'logo', {
        hasLogo: false,
        logoIsSquare: false
      })
      _.set(WIKI.config, 'mail', {
        senderName: '',
        senderEmail: '',
        host: '',
        port: 465,
        secure: true,
        user: '',
        pass: '',
        useDKIM: false,
        dkimDomainName: '',
        dkimKeySelector: '',
        dkimPrivateKey: ''
      })
      _.set(WIKI.config, 'seo', {
        description: '',
        robots: ['index', 'follow'],
118 119
        analyticsService: '',
        analyticsId: ''
120
      })
121
      _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
122
      _.set(WIKI.config, 'telemetry', {
123
        isEnabled: req.body.telemetry === true,
Nick's avatar
Nick committed
124
        clientId: uuid()
125 126 127
      })
      _.set(WIKI.config, 'theming', {
        theme: 'default',
128 129 130 131 132
        darkMode: false,
        iconset: 'mdi',
        injectCSS: '',
        injectHead: '',
        injectBody: ''
133
      })
134
      _.set(WIKI.config, 'title', 'Wiki.js')
135

Nick's avatar
Nick committed
136 137 138 139 140
      // Init Telemetry
      WIKI.kernel.initTelemetry()
      WIKI.telemetry.sendEvent('setup', 'install-start')

      // Basic checks
141 142
      if (!semver.satisfies(process.version, '>=10.12')) {
        throw new Error('Node.js 10.12.x or later required!')
Nick's avatar
Nick committed
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
      }

      // Upgrade from WIKI.js 1.x?
      if (req.body.upgrade) {
        WIKI.telemetry.sendEvent('setup', 'install-mongo-upgrade')
        await WIKI.system.upgradeFromMongo({
          mongoCnStr: cfgHelper.parseConfigValue(req.body.upgMongo)
        })
      }

      // Create directory structure
      WIKI.logger.info('Creating data directories...')
      const dataPath = path.join(process.cwd(), 'data')
      await fs.ensureDir(dataPath)
      await fs.emptyDir(path.join(dataPath, 'cache'))
      await fs.ensureDir(path.join(dataPath, 'uploads'))

160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
      // Generate certificates
      WIKI.logger.info('Generating certificates...')
      const certs = crypto.generateKeyPairSync('rsa', {
        modulusLength: 2048,
        publicKeyEncoding: {
          type: 'pkcs1',
          format: 'pem'
        },
        privateKeyEncoding: {
          type: 'pkcs1',
          format: 'pem',
          cipher: 'aes-256-cbc',
          passphrase: WIKI.config.sessionSecret
        }
      })

176 177 178 179 180
      _.set(WIKI.config, 'certs', {
        jwk: pem2jwk(certs.publicKey),
        public: certs.publicKey,
        private: certs.privateKey
      })
181

NGPixel's avatar
NGPixel committed
182
      // Save config to DB
183
      WIKI.logger.info('Persisting config to DB...')
184
      await WIKI.configSvc.saveToDb([
185
        'auth',
186 187 188
        'certs',
        'company',
        'features',
189
        'graphEndpoint',
190
        'host',
191
        'lang',
192 193 194
        'logo',
        'mail',
        'seo',
195 196
        'sessionSecret',
        'telemetry',
197
        'theming',
198
        'title'
199
      ])
NGPixel's avatar
NGPixel committed
200

201 202
      // Create default locale
      WIKI.logger.info('Installing default locale...')
203
      await WIKI.models.locales.query().insert({
204
        code: 'en',
205
        strings: {},
206 207 208 209 210
        isRTL: false,
        name: 'English',
        nativeName: 'English'
      })

211
      // Create default groups
212 213 214 215 216

      WIKI.logger.info('Creating default groups...')
      const adminGroup = await WIKI.models.groups.query().insert({
        name: 'Administrators',
        permissions: JSON.stringify(['manage:system']),
217
        pageRules: JSON.stringify([]),
218 219 220 221
        isSystem: true
      })
      const guestGroup = await WIKI.models.groups.query().insert({
        name: 'Guests',
222
        permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
223
        pageRules: JSON.stringify([
224
          { id: 'guest', roles: ['read:pages', 'read:assets', 'read:comments'], match: 'START', deny: false, path: '', locales: [] }
225
        ]),
226 227 228
        isSystem: true
      })

229
      // Load authentication strategies + enable local
230 231
      await WIKI.models.authentication.refreshStrategiesFromDisk()
      await WIKI.models.authentication.query().patch({ isEnabled: true }).where('key', 'local')
232 233

      // Load editors + enable default
234 235
      await WIKI.models.editors.refreshEditorsFromDisk()
      await WIKI.models.editors.query().patch({ isEnabled: true }).where('key', 'markdown')
236

237 238 239
      // Load loggers
      await WIKI.models.loggers.refreshLoggersFromDisk()

240 241 242
      // Load renderers
      await WIKI.models.renderers.refreshRenderersFromDisk()

243 244 245 246
      // Load search engines + enable default
      await WIKI.models.searchEngines.refreshSearchEnginesFromDisk()
      await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db')

Nick's avatar
Nick committed
247 248
      WIKI.telemetry.sendEvent('setup', 'install-loadedmodules')

249
      // Load storage targets
250
      await WIKI.models.storage.refreshTargetsFromDisk()
251

NGPixel's avatar
NGPixel committed
252
      // Create root administrator
253
      WIKI.logger.info('Creating root administrator...')
254
      await WIKI.models.users.query().delete().where({
NGPixel's avatar
NGPixel committed
255
        providerKey: 'local',
256 257
        email: req.body.adminEmail
      })
258
      const adminUser = await WIKI.models.users.query().insert({
NGPixel's avatar
NGPixel committed
259 260
        email: req.body.adminEmail,
        provider: 'local',
261
        password: req.body.adminPassword,
NGPixel's avatar
NGPixel committed
262
        name: 'Administrator',
263
        locale: 'en',
264
        defaultEditor: 'markdown',
265 266 267
        tfaIsActive: false,
        isActive: true,
        isVerified: true
NGPixel's avatar
NGPixel committed
268
      })
269
      await adminUser.$relatedQuery('groups').relate(adminGroup.id)
NGPixel's avatar
NGPixel committed
270

271
      // Create Guest account
272
      WIKI.logger.info('Creating guest account...')
273
      await WIKI.models.users.query().delete().where({
NGPixel's avatar
NGPixel committed
274
        providerKey: 'local',
275 276
        email: 'guest@example.com'
      })
277 278 279 280 281 282 283
      const guestUser = await WIKI.models.users.query().insert({
        provider: 'local',
        email: 'guest@example.com',
        name: 'Guest',
        password: '',
        locale: 'en',
        defaultEditor: 'markdown',
284
        tfaIsActive: false,
285 286 287
        isSystem: true,
        isActive: true,
        isVerified: true
288 289
      })
      await guestUser.$relatedQuery('groups').relate(guestGroup.id)
290

291 292 293 294 295 296
      // Create site nav

      WIKI.logger.info('Creating default site navigation')
      await WIKI.models.navigation.query().delete().where({ key: 'site' })
      await WIKI.models.navigation.query().insert({
        key: 'site',
Nicolas Giard's avatar
Nicolas Giard committed
297
        config: [
298
          {
299
            id: uuid(),
Nick's avatar
Nick committed
300
            icon: 'mdi-home',
301 302 303 304 305
            kind: 'link',
            label: 'Home',
            target: '/',
            targetType: 'home'
          }
Nicolas Giard's avatar
Nicolas Giard committed
306
        ]
307 308
      })

309
      WIKI.logger.info('Setup is complete!')
Nick's avatar
Nick committed
310
      WIKI.telemetry.sendEvent('setup', 'install-completed')
311 312
      res.json({
        ok: true,
313
        redirectPath: '/',
314
        redirectPort: WIKI.config.port
315 316
      }).end()

317 318
      WIKI.config.setup = false

319
      WIKI.logger.info('Stopping Setup...')
320
      WIKI.server.destroy(() => {
321
        WIKI.logger.info('Setup stopped. Starting Wiki.js...')
322
        _.delay(() => {
323
          WIKI.kernel.bootMaster()
324 325
        }, 1000)
      })
326
    } catch (err) {
Nick's avatar
Nick committed
327
      WIKI.telemetry.sendError(err)
328
      res.json({ ok: false, error: err.message })
329
    }
NGPixel's avatar
NGPixel committed
330 331
  })

332 333 334 335 336 337 338 339 340 341 342 343 344 345
  // ----------------------------------------
  // 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.send({
      message: err.message,
346
      error: WIKI.IS_DEBUG ? err : {}
347
    })
348 349
    WIKI.logger.error(err.message)
    WIKI.telemetry.sendError(err)
350 351 352 353 354 355
  })

  // ----------------------------------------
  // Start HTTP server
  // ----------------------------------------

Nick's avatar
Nick committed
356
  WIKI.logger.info(`Starting HTTP server on port ${WIKI.config.port}...`)
357

358
  app.set('port', WIKI.config.port)
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385

  if (WIKI.config.ssl.enabled) {
    WIKI.logger.info(`HTTPS Server on port: [ ${WIKI.config.port} ]`)
    const tlsOpts = {}
    try {
      if (WIKI.config.ssl.format === 'pem') {
        tlsOpts.key = fs.readFileSync(WIKI.config.ssl.key)
        tlsOpts.cert = fs.readFileSync(WIKI.config.ssl.cert)
      } else {
        tlsOpts.pfx = fs.readFileSync(WIKI.config.ssl.pfx)
      }
      if (!_.isEmpty(WIKI.config.ssl.passphrase)) {
        tlsOpts.passphrase = WIKI.config.ssl.passphrase
      }
      if (!_.isEmpty(WIKI.config.ssl.dhparam)) {
        tlsOpts.dhparam = WIKI.config.ssl.dhparam
      }
    } catch (err) {
      WIKI.logger.error('Failed to setup HTTPS server parameters:')
      WIKI.logger.error(err)
      return process.exit(1)
    }
    WIKI.server = https.createServer(tlsOpts, app)
  } else {
    WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
    WIKI.server = http.createServer(app)
  }
386
  WIKI.server.listen(WIKI.config.port, WIKI.config.bindIP)
387 388 389

  var openConnections = []

390
  WIKI.server.on('connection', (conn) => {
391 392 393
    let key = conn.remoteAddress + ':' + conn.remotePort
    openConnections[key] = conn
    conn.on('close', () => {
Nick's avatar
Nick committed
394
      openConnections.splice(key, 1)
395 396 397
    })
  })

398 399
  WIKI.server.destroy = (cb) => {
    WIKI.server.close(cb)
400 401 402 403 404
    for (let key in openConnections) {
      openConnections[key].destroy()
    }
  }

405
  WIKI.server.on('error', (error) => {
406 407 408 409 410 411
    if (error.syscall !== 'listen') {
      throw error
    }

    switch (error.code) {
      case 'EACCES':
412
        WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
NGPixel's avatar
NGPixel committed
413
        return process.exit(1)
414
      case 'EADDRINUSE':
415
        WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
NGPixel's avatar
NGPixel committed
416
        return process.exit(1)
417 418 419 420 421
      default:
        throw error
    }
  })

422
  WIKI.server.on('listening', () => {
423
    WIKI.logger.info('HTTP Server: [ RUNNING ]')
424 425 426 427 428
    WIKI.logger.info('🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻')
    WIKI.logger.info('')
    WIKI.logger.info(`Browse to http://localhost:${WIKI.config.port}/ to complete setup!`)
    WIKI.logger.info('')
    WIKI.logger.info('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
429 430
  })
}