|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const Promise = require('bluebird'); |
| 4 | +const mysql = require('mysql'); |
| 5 | +const crypto = require('crypto'); |
| 6 | +const omit = require('lodash/omit'); |
| 7 | +const cli = require('../../lib'); |
| 8 | + |
| 9 | +class MySQLExtension extends cli.Extension { |
| 10 | + _query(queryString) { |
| 11 | + return Promise.fromCallback(cb => this.connection.query(queryString, cb)); |
| 12 | + } |
| 13 | + |
| 14 | + setup(cmd, argv) { |
| 15 | + // ghost setup --local, skip |
| 16 | + if (argv.local) { |
| 17 | + return; |
| 18 | + } |
| 19 | + |
| 20 | + cmd.addStage('mysql', this.setupMySQL.bind(this)); |
| 21 | + } |
| 22 | + |
| 23 | + setupMySQL(argv, ctx, task) { |
| 24 | + this.databaseConfig = ctx.instance.config.get('database'); |
| 25 | + |
| 26 | + return this.canConnect(ctx) |
| 27 | + .then(() => { |
| 28 | + if (this.databaseConfig.connection.user === 'root') { |
| 29 | + return this.ui.confirm('Your MySQL user is root. Would you like to create a custom Ghost MySQL user?', true) |
| 30 | + .then((res) => { |
| 31 | + if (res.yes) { |
| 32 | + return this.createMySQLUser(ctx); |
| 33 | + } |
| 34 | + }); |
| 35 | + } |
| 36 | + |
| 37 | + this.ui.log('MySQL: Your user is: ' + this.databaseConfig.connection.user, 'green'); |
| 38 | + }) |
| 39 | + .finally(() => { |
| 40 | + this.connection.end(); |
| 41 | + }); |
| 42 | + } |
| 43 | + |
| 44 | + canConnect(ctx) { |
| 45 | + this.connection = mysql.createConnection(omit(this.databaseConfig.connection, 'database')); |
| 46 | + |
| 47 | + return Promise.fromCallback(cb => this.connection.connect(cb)) |
| 48 | + .then(() => { |
| 49 | + this.ui.log('MySQL: connection successful.', 'green'); |
| 50 | + }) |
| 51 | + .catch((err) => { |
| 52 | + this.ui.log('MySQL: connection error.', 'yellow'); |
| 53 | + |
| 54 | + if (err.code === 'ER_ACCESS_DENIED_ERROR') { |
| 55 | + throw new cli.errors.ConfigError({ |
| 56 | + message: err.message, |
| 57 | + configs: { |
| 58 | + 'database.connection.user': this.databaseConfig.connection.user, |
| 59 | + 'database.connection.password': this.databaseConfig.connection.password |
| 60 | + }, |
| 61 | + environment: ctx.instance.system.environment, |
| 62 | + help: 'You can run `ghost config` to re-enter the correct credentials. Alternatively you can run `ghost setup` again.' |
| 63 | + }); |
| 64 | + } |
| 65 | + |
| 66 | + throw new cli.errors.ConfigError({ |
| 67 | + message: err.message, |
| 68 | + configs: { |
| 69 | + 'database.connection.host': this.databaseConfig.connection.host, |
| 70 | + 'database.connection.port': this.databaseConfig.connection.port || '3306' |
| 71 | + }, |
| 72 | + environment: ctx.instance.system.environment, |
| 73 | + help: 'Please ensure that MySQL is installed and reachable. You can always re-run `ghost setup` and try it again.' |
| 74 | + }); |
| 75 | + }); |
| 76 | + } |
| 77 | + |
| 78 | + createMySQLUser(ctx) { |
| 79 | + let randomPassword = crypto.randomBytes(10).toString('hex'); |
| 80 | + let host = this.databaseConfig.connection.host; |
| 81 | + |
| 82 | + // IMPORTANT: we generate random MySQL usernames |
| 83 | + // e.g. you delete all your Ghost instances from your droplet and start from scratch, the MySQL users would remain and the CLI has to generate a random user name to be able to |
| 84 | + // e.g. if we would rely on the instance name, the instance naming only auto increments if there are existing instances |
| 85 | + // the most important fact is, that if a MySQL user exists, we have no access to the password, which we need to autofill the Ghost config |
| 86 | + // disadvantage: the CLI could potentially create lot's of MySQL users (but this should only happen if the user installs Ghost over and over again with root credentials) |
| 87 | + let username = 'ghost-' + Math.floor(Math.random() * 1000); |
| 88 | + |
| 89 | + return this._query('CREATE USER \'' + username + '\'@\'' + host + '\' IDENTIFIED BY \'' + randomPassword + '\';') |
| 90 | + .then(() => { |
| 91 | + this.ui.log('MySQL: successfully created `' + username + '`.', 'green'); |
| 92 | + |
| 93 | + return this.grantPermissions({username: username}) |
| 94 | + .then(() => { |
| 95 | + ctx.instance.config.set('database.connection.user', username); |
| 96 | + ctx.instance.config.set('database.connection.password', randomPassword); |
| 97 | + }); |
| 98 | + }) |
| 99 | + .catch((err) => { |
| 100 | + // CASE: user exists, we are not able to figure out the original password, skip mysql setup |
| 101 | + if (err.errno === 1396) { |
| 102 | + this.ui.log('MySQL: `' + username + '` user exists. Skipping.', 'yellow'); |
| 103 | + return Promise.resolve(); |
| 104 | + } |
| 105 | + |
| 106 | + this.ui.log('MySQL: unable to create custom Ghost user.', 'yellow'); |
| 107 | + throw new cli.errors.SystemError(err.message); |
| 108 | + }); |
| 109 | + } |
| 110 | + |
| 111 | + grantPermissions(options) { |
| 112 | + let host = this.databaseConfig.connection.host; |
| 113 | + let database = this.databaseConfig.connection.database; |
| 114 | + let username = options.username; |
| 115 | + |
| 116 | + return this._query('GRANT ALL PRIVILEGES ON ' + database + '.* TO \'' + username + '\'@\'' + host + '\';') |
| 117 | + .then(() => { |
| 118 | + this.ui.log('MySQL: successfully granted permissions for `' + username + '` user.', 'green'); |
| 119 | + |
| 120 | + return this._query('FLUSH PRIVILEGES;') |
| 121 | + .then(() => { |
| 122 | + this.ui.log('MySQL: flushed privileges', 'green'); |
| 123 | + }) |
| 124 | + .catch((err) => { |
| 125 | + this.ui.log('MySQL: unable to flush privileges.', 'yellow'); |
| 126 | + throw new cli.errors.SystemError(err.message); |
| 127 | + }); |
| 128 | + }) |
| 129 | + .catch((err) => { |
| 130 | + this.ui.log('MySQL: unable to grant permissions for `' + username + '` user.', 'yellow'); |
| 131 | + throw new cli.errors.SystemError(err.message); |
| 132 | + }); |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +module.exports = MySQLExtension; |
0 commit comments