setup.js 13.4 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
      // Load authentication strategies + enable local
257 258
      await WIKI.models.authentication.refreshStrategiesFromDisk()
      await WIKI.models.authentication.query().patch({ isEnabled: true }).where('key', 'local')
259 260

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

264 265 266
      // Load loggers
      await WIKI.models.loggers.refreshLoggersFromDisk()

267 268 269
      // Load renderers
      await WIKI.models.renderers.refreshRenderersFromDisk()

270 271 272 273
      // Load search engines + enable default
      await WIKI.models.searchEngines.refreshSearchEnginesFromDisk()
      await WIKI.models.searchEngines.query().patch({ isEnabled: true }).where('key', 'db')

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

276
      // Load storage targets
277
      await WIKI.models.storage.refreshTargetsFromDisk()
278

NGPixel's avatar
NGPixel committed
279
      // Create root administrator
280
      WIKI.logger.info('Creating root administrator...')
281
      const adminUser = await WIKI.models.users.query().insert({
NGPixel's avatar
NGPixel committed
282 283
        email: req.body.adminEmail,
        provider: 'local',
284
        password: req.body.adminPassword,
NGPixel's avatar
NGPixel committed
285
        name: 'Administrator',
286
        locale: 'en',
287
        defaultEditor: 'markdown',
288 289 290
        tfaIsActive: false,
        isActive: true,
        isVerified: true
NGPixel's avatar
NGPixel committed
291
      })
292
      await adminUser.$relatedQuery('groups').relate(adminGroup.id)
NGPixel's avatar
NGPixel committed
293

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

313 314 315 316 317
      // 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
318
        config: [
319
          {
320 321 322 323 324 325 326 327
            locale: 'en',
            items: [
              {
                id: uuid(),
                icon: 'mdi-home',
                kind: 'link',
                label: 'Home',
                target: '/',
328 329 330
                targetType: 'home',
                visibilityMode: 'all',
                visibilityGroups: null
331 332
              }
            ]
333
          }
Nicolas Giard's avatar
Nicolas Giard committed
334
        ]
335 336
      })

337
      WIKI.logger.info('Setup is complete!')
338
      // WIKI.telemetry.sendEvent('setup', 'install-completed')
339 340
      res.json({
        ok: true,
341
        redirectPath: '/',
342
        redirectPort: WIKI.config.port
343 344
      }).end()

345 346 347 348
      if (WIKI.config.telemetry.isEnabled) {
        await WIKI.telemetry.sendInstanceEvent('INSTALL')
      }

349 350
      WIKI.config.setup = false

351
      WIKI.logger.info('Stopping Setup...')
352
      WIKI.server.destroy(() => {
353
        WIKI.logger.info('Setup stopped. Starting Wiki.js...')
354
        _.delay(() => {
355
          WIKI.kernel.bootMaster()
356 357
        }, 1000)
      })
358
    } catch (err) {
359 360 361
      try {
        await WIKI.models.knex('settings').truncate()
      } catch (err) {}
Nick's avatar
Nick committed
362
      WIKI.telemetry.sendError(err)
363
      res.json({ ok: false, error: err.message })
364
    }
NGPixel's avatar
NGPixel committed
365 366
  })

367 368 369 370 371 372 373 374 375 376 377 378 379 380
  // ----------------------------------------
  // 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,
381
      error: WIKI.IS_DEBUG ? err : {}
382
    })
383 384
    WIKI.logger.error(err.message)
    WIKI.telemetry.sendError(err)
385 386 387 388 389 390
  })

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

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

393
  app.set('port', WIKI.config.port)
394

395 396
  WIKI.logger.info(`HTTP Server on port: [ ${WIKI.config.port} ]`)
  WIKI.server = http.createServer(app)
397
  WIKI.server.listen(WIKI.config.port, WIKI.config.bindIP)
398 399 400

  var openConnections = []

401
  WIKI.server.on('connection', (conn) => {
402 403 404
    let key = conn.remoteAddress + ':' + conn.remotePort
    openConnections[key] = conn
    conn.on('close', () => {
Nick's avatar
Nick committed
405
      openConnections.splice(key, 1)
406 407 408
    })
  })

409 410
  WIKI.server.destroy = (cb) => {
    WIKI.server.close(cb)
411 412 413 414 415
    for (let key in openConnections) {
      openConnections[key].destroy()
    }
  }

416
  WIKI.server.on('error', (error) => {
417 418 419 420 421 422
    if (error.syscall !== 'listen') {
      throw error
    }

    switch (error.code) {
      case 'EACCES':
423
        WIKI.logger.error('Listening on port ' + WIKI.config.port + ' requires elevated privileges!')
NGPixel's avatar
NGPixel committed
424
        return process.exit(1)
425
      case 'EADDRINUSE':
426
        WIKI.logger.error('Port ' + WIKI.config.port + ' is already in use!')
NGPixel's avatar
NGPixel committed
427
        return process.exit(1)
428 429 430 431 432
      default:
        throw error
    }
  })

433
  WIKI.server.on('listening', () => {
434
    WIKI.logger.info('HTTP Server: [ RUNNING ]')
435 436 437 438 439
    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('🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺🔺')
440 441
  })
}