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 261 262 263 264 265 266
      // Load local authentication strategy
      await WIKI.models.authentication.query().insert({
        key: 'local',
        config: {},
        selfRegistration: false,
        domainWhitelist: {v: []},
        autoEnrollGroups: {v: []},
        order: 0,
        strategyKey: 'local',
        displayName: 'Local'
      })
267 268

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

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

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

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

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

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

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

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

321 322 323 324 325
      // 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
326
        config: [
327
          {
328 329 330 331 332 333 334 335
            locale: 'en',
            items: [
              {
                id: uuid(),
                icon: 'mdi-home',
                kind: 'link',
                label: 'Home',
                target: '/',
336 337 338
                targetType: 'home',
                visibilityMode: 'all',
                visibilityGroups: null
339 340
              }
            ]
341
          }
Nicolas Giard's avatar
Nicolas Giard committed
342
        ]
343 344
      })

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

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

357 358
      WIKI.config.setup = false

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

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

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

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

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

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

  var openConnections = []

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

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

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

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

441
  WIKI.server.on('listening', () => {
442
    WIKI.logger.info('HTTP Server: [ RUNNING ]')
443 444 445 446 447
    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('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
448 449
  })
}