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

15
/* global WIKI */
16

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

23
  WIKI.system = require('./core/system')
24

25 26 27 28
  // ----------------------------------------
  // Define Express App
  // ----------------------------------------

29
  let app = express()
30 31 32 33 34 35
  app.use(compression())

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

36
  app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico')))
37
  app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets')))
38 39 40 41 42

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

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

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

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

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

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

63 64 65 66
  // ----------------------------------------
  // Controllers
  // ----------------------------------------

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

NGPixel's avatar
NGPixel committed
72
  /**
73
   * Finalize
NGPixel's avatar
NGPixel committed
74
   */
75 76
  app.post('/finalize', async (req, res) => {
    try {
77
      // Set config
78 79 80 81 82
      _.set(WIKI.config, 'auth', {
        audience: 'urn:wiki.js',
        tokenExpiration: '30m',
        tokenRenewal: '14d'
      })
83 84 85 86 87 88
      _.set(WIKI.config, 'company', '')
      _.set(WIKI.config, 'features', {
        featurePageRatings: true,
        featurePageComments: true,
        featurePersonalWikis: true
      })
89
      _.set(WIKI.config, 'graphEndpoint', 'https://graph.requarks.io')
90
      _.set(WIKI.config, 'host', req.body.siteUrl)
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
      _.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,
107
        verifySSL: true,
108 109 110 111 112 113 114 115 116 117
        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
      // Init Telemetry
      WIKI.kernel.initTelemetry()
138
      // WIKI.telemetry.sendEvent('setup', 'install-start')
Nick's avatar
Nick committed
139 140

      // 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
      }

      // Create directory structure
      WIKI.logger.info('Creating data directories...')
147 148 149
      await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath))
      await fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
      await fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
Nick's avatar
Nick committed
150

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
      // 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
        }
      })

167 168 169 170 171
      _.set(WIKI.config, 'certs', {
        jwk: pem2jwk(certs.publicKey),
        public: certs.publicKey,
        private: certs.privateKey
      })
172

NGPixel's avatar
NGPixel committed
173
      // Save config to DB
174
      WIKI.logger.info('Persisting config to DB...')
175
      await WIKI.configSvc.saveToDb([
176
        'auth',
177 178 179
        'certs',
        'company',
        'features',
180
        'graphEndpoint',
181
        'host',
182
        'lang',
183 184 185
        'logo',
        'mail',
        'seo',
186 187
        'sessionSecret',
        'telemetry',
188
        'theming',
189
        'uploads',
190
        'title'
191
      ], false)
NGPixel's avatar
NGPixel committed
192

193
      // Truncate tables (reset from previous failed install)
194 195 196 197 198 199 200 201 202 203
      await WIKI.models.locales.query().where('code', '!=', 'x').del()
      await WIKI.models.navigation.query().truncate()
      switch (WIKI.config.db.type) {
        case 'postgres':
          await WIKI.models.knex.raw('TRUNCATE groups, users CASCADE')
          break
        case 'mysql':
        case 'mariadb':
          await WIKI.models.groups.query().where('id', '>', 0).del()
          await WIKI.models.users.query().where('id', '>', 0).del()
204 205
          await WIKI.models.knex.raw('ALTER TABLE `groups` AUTO_INCREMENT = 1')
          await WIKI.models.knex.raw('ALTER TABLE `users` AUTO_INCREMENT = 1')
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
          break
        case 'mssql':
          await WIKI.models.groups.query().del()
          await WIKI.models.users.query().del()
          await WIKI.models.knex.raw(`
            IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'groups' AND last_value IS NOT NULL)
              DBCC CHECKIDENT ([groups], RESEED, 0)
          `)
          await WIKI.models.knex.raw(`
            IF EXISTS (SELECT * FROM sys.identity_columns WHERE OBJECT_NAME(OBJECT_ID) = 'users' AND last_value IS NOT NULL)
              DBCC CHECKIDENT ([users], RESEED, 0)
          `)
          break
        case 'sqlite':
          await WIKI.models.groups.query().truncate()
          await WIKI.models.users.query().truncate()
          break
223 224
      }

225 226
      // Create default locale
      WIKI.logger.info('Installing default locale...')
227
      await WIKI.models.locales.query().insert({
228
        code: 'en',
229
        strings: {},
230 231 232 233 234
        isRTL: false,
        name: 'English',
        nativeName: 'English'
      })

235
      // Create default groups
236 237 238 239 240

      WIKI.logger.info('Creating default groups...')
      const adminGroup = await WIKI.models.groups.query().insert({
        name: 'Administrators',
        permissions: JSON.stringify(['manage:system']),
241
        pageRules: JSON.stringify([]),
242 243 244 245
        isSystem: true
      })
      const guestGroup = await WIKI.models.groups.query().insert({
        name: 'Guests',
246
        permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
247
        pageRules: JSON.stringify([
248
          { id: 'guest', roles: ['read:pages', 'read:assets', 'read:comments'], match: 'START', deny: false, path: '', locales: [] }
249
        ]),
250 251
        isSystem: true
      })
252 253 254
      if (adminGroup.id !== 1 || guestGroup.id !== 2) {
        throw new Error('Incorrect groups auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')
      }
255

256 257 258 259 260
      // Load local authentication strategy
      await WIKI.models.authentication.query().insert({
        key: 'local',
        config: {},
        selfRegistration: false,
261
        isEnabled: true,
262 263 264 265 266 267
        domainWhitelist: {v: []},
        autoEnrollGroups: {v: []},
        order: 0,
        strategyKey: 'local',
        displayName: 'Local'
      })
268 269

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

273 274 275
      // Load loggers
      await WIKI.models.loggers.refreshLoggersFromDisk()

276 277 278
      // Load renderers
      await WIKI.models.renderers.refreshRenderersFromDisk()

279 280 281 282
      // Load search engines + enable default
      await WIKI.models.searchEngines.refreshSearchEnginesFromDisk()
      await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db')

283
      // WIKI.telemetry.sendEvent('setup', 'install-loadedmodules')
Nick's avatar
Nick committed
284

285
      // Load storage targets
286
      await WIKI.models.storage.refreshTargetsFromDisk()
287

NGPixel's avatar
NGPixel committed
288
      // Create root administrator
289
      WIKI.logger.info('Creating root administrator...')
290
      const adminUser = await WIKI.models.users.query().insert({
NGPixel's avatar
NGPixel committed
291 292
        email: req.body.adminEmail,
        provider: 'local',
293
        password: req.body.adminPassword,
NGPixel's avatar
NGPixel committed
294
        name: 'Administrator',
295
        locale: 'en',
296
        defaultEditor: 'markdown',
297 298 299
        tfaIsActive: false,
        isActive: true,
        isVerified: true
NGPixel's avatar
NGPixel committed
300
      })
301
      await adminUser.$relatedQuery('groups').relate(adminGroup.id)
NGPixel's avatar
NGPixel committed
302

303
      // Create Guest account
304
      WIKI.logger.info('Creating guest account...')
305 306 307 308 309 310 311
      const guestUser = await WIKI.models.users.query().insert({
        provider: 'local',
        email: 'guest@example.com',
        name: 'Guest',
        password: '',
        locale: 'en',
        defaultEditor: 'markdown',
312
        tfaIsActive: false,
313 314 315
        isSystem: true,
        isActive: true,
        isVerified: true
316 317
      })
      await guestUser.$relatedQuery('groups').relate(guestGroup.id)
318
      if (adminUser.id !== 1 || guestUser.id !== 2) {
319
        throw new Error('Incorrect users auto-increment configuration! Should start at 0 and increment by 1. Contact your database administrator.')
320
      }
321

322 323 324 325 326
      // Create site nav

      WIKI.logger.info('Creating default site navigation')
      await WIKI.models.navigation.query().insert({
        key: 'site',
Nicolas Giard's avatar
Nicolas Giard committed
327
        config: [
328
          {
329 330 331 332 333 334 335 336
            locale: 'en',
            items: [
              {
                id: uuid(),
                icon: 'mdi-home',
                kind: 'link',
                label: 'Home',
                target: '/',
337 338 339
                targetType: 'home',
                visibilityMode: 'all',
                visibilityGroups: null
340 341
              }
            ]
342
          }
Nicolas Giard's avatar
Nicolas Giard committed
343
        ]
344 345
      })

346
      WIKI.logger.info('Setup is complete!')
347
      // WIKI.telemetry.sendEvent('setup', 'install-completed')
348 349
      res.json({
        ok: true,
350
        redirectPath: '/',
351
        redirectPort: WIKI.config.port
352 353
      }).end()

354 355 356 357
      if (WIKI.config.telemetry.isEnabled) {
        await WIKI.telemetry.sendInstanceEvent('INSTALL')
      }

358 359
      WIKI.config.setup = false

360
      WIKI.logger.info('Stopping Setup...')
361
      WIKI.server.destroy(() => {
362
        WIKI.logger.info('Setup stopped. Starting Wiki.js...')
363
        _.delay(() => {
364
          WIKI.kernel.bootMaster()
365 366
        }, 1000)
      })
367
    } catch (err) {
368 369 370
      try {
        await WIKI.models.knex('settings').truncate()
      } catch (err) {}
Nick's avatar
Nick committed
371
      WIKI.telemetry.sendError(err)
372
      res.json({ ok: false, error: err.message })
373
    }
NGPixel's avatar
NGPixel committed
374 375
  })

376 377 378 379 380 381 382 383 384 385 386 387 388 389
  // ----------------------------------------
  // 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,
390
      error: WIKI.IS_DEBUG ? err : {}
391
    })
392 393
    WIKI.logger.error(err.message)
    WIKI.telemetry.sendError(err)
394 395 396 397 398 399
  })

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

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

402
  app.set('port', WIKI.config.port)
403

404 405
  WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
  WIKI.server = http.createServer(app)
406
  WIKI.server.listen(WIKI.config.port, WIKI.config.bindIP)
407 408 409

  var openConnections = []

410
  WIKI.server.on('connection', (conn) => {
411 412 413
    let key = conn.remoteAddress + ':' + conn.remotePort
    openConnections[key] = conn
    conn.on('close', () => {
Nick's avatar
Nick committed
414
      openConnections.splice(key, 1)
415 416 417
    })
  })

418 419
  WIKI.server.destroy = (cb) => {
    WIKI.server.close(cb)
420 421 422 423 424
    for (let key in openConnections) {
      openConnections[key].destroy()
    }
  }

425
  WIKI.server.on('error', (error) => {
426 427 428 429 430 431
    if (error.syscall !== 'listen') {
      throw error
    }

    switch (error.code) {
      case 'EACCES':
432
        WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
NGPixel's avatar
NGPixel committed
433
        return process.exit(1)
434
      case 'EADDRINUSE':
435
        WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
NGPixel's avatar
NGPixel committed
436
        return process.exit(1)
437 438 439 440 441
      default:
        throw error
    }
  })

442
  WIKI.server.on('listening', () => {
443
    WIKI.logger.info('HTTP Server: [ RUNNING ]')
444 445
    WIKI.logger.info('🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻🔻')
    WIKI.logger.info('')
446
    WIKI.logger.info(`Browse to http://YOUR-SERVER-IP:${WIKI.config.port}/ to complete setup!`)
447 448
    WIKI.logger.info('')
    WIKI.logger.info('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
449 450
  })
}