diff --git a/services/web/Dockerfile b/services/web/Dockerfile index 0a2e5d6983..c5c0d1f477 100644 --- a/services/web/Dockerfile +++ b/services/web/Dockerfile @@ -50,6 +50,7 @@ RUN chmod 0755 ./install_deps.sh && ./install_deps.sh FROM webpack as app RUN find /app/public -name '*.js.map' -delete +RUN rm /app/modules/server-ce-scripts -rf USER node CMD ["node", "--expose-gc", "app.js"] diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index e8de8a0c57..eb9c85125c 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -702,7 +702,7 @@ module.exports = { tprLinkedFileRefreshError: [], }, - moduleImportSequence: ['launchpad', 'user-activate'], + moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'], csp: { percentage: parseFloat(process.env.CSP_PERCENTAGE) || 0, diff --git a/services/web/modules/server-ce-scripts/index.js b/services/web/modules/server-ce-scripts/index.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/services/web/modules/server-ce-scripts/index.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/services/web/modules/server-ce-scripts/scripts/check-mongodb.js b/services/web/modules/server-ce-scripts/scripts/check-mongodb.js new file mode 100644 index 0000000000..f2e62c94e4 --- /dev/null +++ b/services/web/modules/server-ce-scripts/scripts/check-mongodb.js @@ -0,0 +1,12 @@ +const { waitForDb } = require('../../../app/src/infrastructure/mongodb') + +waitForDb() + .then(() => { + console.error('Mongodb is up.') + process.exit(0) + }) + .catch(err => { + console.error('Cannot connect to mongodb.') + console.error(err) + process.exit(1) + }) diff --git a/services/web/modules/server-ce-scripts/scripts/check-redis.js b/services/web/modules/server-ce-scripts/scripts/check-redis.js new file mode 100644 index 0000000000..6402619eaa --- /dev/null +++ b/services/web/modules/server-ce-scripts/scripts/check-redis.js @@ -0,0 +1,18 @@ +const RedisWrapper = require('../../../app/src/infrastructure/RedisWrapper') +const rclient = RedisWrapper.client('health_check') +rclient.on('error', err => { + console.error('Cannot connect to redis.') + console.error(err) + process.exit(1) +}) + +rclient.healthCheck(err => { + if (err) { + console.error('Cannot connect to redis.') + console.error(err) + process.exit(1) + } else { + console.error('Redis is up.') + process.exit(0) + } +}) diff --git a/services/web/modules/server-ce-scripts/scripts/create-admin.js b/services/web/modules/server-ce-scripts/scripts/create-admin.js new file mode 100644 index 0000000000..dedeba7e06 --- /dev/null +++ b/services/web/modules/server-ce-scripts/scripts/create-admin.js @@ -0,0 +1,72 @@ +const Settings = require('@overleaf/settings') +const { db, waitForDb } = require('../../../app/src/infrastructure/mongodb') +const UserRegistrationHandler = require('../../../app/src/Features/User/UserRegistrationHandler') +const OneTimeTokenHandler = require('../../../app/src/Features/Security/OneTimeTokenHandler') + +async function main() { + await waitForDb() + + const email = (process.argv.slice(2).pop() || '').replace(/^--email=/, '') + if (!email) { + console.error(`Usage: node ${__filename} --email=joe@example.com`) + process.exit(1) + } + + await new Promise((resolve, reject) => { + UserRegistrationHandler.registerNewUser( + { + email, + password: require('crypto').randomBytes(32).toString('hex'), + }, + (error, user) => { + if (error && error.message !== 'EmailAlreadyRegistered') { + return reject(error) + } + db.users.updateOne( + { _id: user._id }, + { $set: { isAdmin: true } }, + error => { + if (error) { + return reject(error) + } + const ONE_WEEK = 7 * 24 * 60 * 60 // seconds + OneTimeTokenHandler.getNewToken( + 'password', + { + expiresIn: ONE_WEEK, + email: user.email, + user_id: user._id.toString(), + }, + (err, token) => { + if (err) { + return reject(err) + } + + console.log('') + console.log(`\ +Successfully created ${email} as an admin user. + +Please visit the following URL to set a password for ${email} and log in: + +${Settings.siteUrl}/user/password/set?passwordResetToken=${token} +\ +`) + resolve() + } + ) + } + ) + } + ) + }) +} + +main() + .then(() => { + console.error('Done.') + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/services/web/modules/server-ce-scripts/scripts/delete-user.js b/services/web/modules/server-ce-scripts/scripts/delete-user.js new file mode 100644 index 0000000000..5ec1065081 --- /dev/null +++ b/services/web/modules/server-ce-scripts/scripts/delete-user.js @@ -0,0 +1,43 @@ +const { waitForDb } = require('../../../app/src/infrastructure/mongodb') +const UserGetter = require('../../../app/src/Features/User/UserGetter') +const UserDeleter = require('../../../app/src/Features/User/UserDeleter') + +async function main() { + await waitForDb() + + const email = (process.argv.slice(2).pop() || '').replace(/^--email=/, '') + if (!email) { + console.error(`Usage: node ${__filename} --email=joe@example.com`) + process.exit(1) + } + + await new Promise((resolve, reject) => { + UserGetter.getUser({ email }, function (error, user) { + if (error) { + return reject(error) + } + if (!user) { + console.log( + `user ${email} not in database, potentially already deleted` + ) + return resolve() + } + UserDeleter.deleteUser(user._id, function (err) { + if (err) { + return reject(err) + } + resolve() + }) + }) + }) +} + +main() + .then(() => { + console.error('Done.') + process.exit(0) + }) + .catch(err => { + console.error(err) + process.exit(1) + }) diff --git a/services/web/modules/server-ce-scripts/test/acceptance/config/settings.test.js b/services/web/modules/server-ce-scripts/test/acceptance/config/settings.test.js new file mode 100644 index 0000000000..7f62aa602d --- /dev/null +++ b/services/web/modules/server-ce-scripts/test/acceptance/config/settings.test.js @@ -0,0 +1,5 @@ +module.exports = { + test: { + counterInit: 160000, + }, +} diff --git a/services/web/modules/server-ce-scripts/test/acceptance/src/Init.js b/services/web/modules/server-ce-scripts/test/acceptance/src/Init.js new file mode 100644 index 0000000000..00781b805e --- /dev/null +++ b/services/web/modules/server-ce-scripts/test/acceptance/src/Init.js @@ -0,0 +1 @@ +require(`../../../../../test/acceptance/src/helpers/InitApp`) diff --git a/services/web/modules/server-ce-scripts/test/acceptance/src/ServerCEScriptsTests.js b/services/web/modules/server-ce-scripts/test/acceptance/src/ServerCEScriptsTests.js new file mode 100644 index 0000000000..bd7676327e --- /dev/null +++ b/services/web/modules/server-ce-scripts/test/acceptance/src/ServerCEScriptsTests.js @@ -0,0 +1,110 @@ +const { execSync } = require('child_process') +const { expect } = require('chai') +const User = require('../../../../../test/acceptance/src/helpers/User').promises + +/** + * @param {string} cmd + * @return {string} + */ +function run(cmd) { + // https://nodejs.org/docs/latest-v12.x/api/child_process.html#child_process_child_process_execsync_command_options + // > stderr by default will be output to the parent process' stderr + // > unless stdio is specified. + // https://nodejs.org/docs/latest-v12.x/api/child_process.html#child_process_options_stdio + // Pipe stdin from /dev/null, store stdout, pipe stderr to /dev/null. + return execSync(cmd, { + stdio: ['ignore', 'pipe', 'ignore'], + cwd: 'modules/server-ce-scripts/scripts', + }).toString() +} + +describe('ServerCEScripts', function () { + describe('check-mongodb', function () { + it('should exit with code 0 on success', function () { + run('node check-mongodb') + }) + + it('should exit with code 1 on error', function () { + try { + run( + 'MONGO_SERVER_SELECTION_TIMEOUT=1' + + 'MONGO_CONNECTION_STRING=mongodb://localhost:4242 ' + + 'node check-mongodb' + ) + } catch (e) { + expect(e.status).to.equal(1) + return + } + expect.fail('command should have failed') + }) + }) + + describe('check-redis', function () { + it('should exit with code 0 on success', function () { + run('node check-redis') + }) + + it('should exit with code 1 on error', function () { + try { + run('REDIS_HOST=localhost node check-redis') + } catch (e) { + expect(e.status).to.equal(1) + return + } + expect.fail('command should have failed') + }) + }) + + describe('create-admin', function () { + it('should exit with code 0 on success', function () { + const out = run('node create-admin --email=foo@bar.com') + expect(out).to.include('/user/password/set?passwordResetToken=') + }) + + it('should exit with code 1 on missing email', function () { + try { + run('node create-admin') + } catch (e) { + expect(e.status).to.equal(1) + return + } + expect.fail('command should have failed') + }) + }) + + describe('delete-user', function () { + let user + beforeEach(async function () { + user = new User() + await user.login() + }) + + it('should log missing user', function () { + const email = 'does-not-exist@example.com' + const out = run('node delete-user --email=' + email) + expect(out).to.include('not in database, potentially already deleted') + }) + + it('should exit with code 0 on success', function () { + const email = user.email + run('node delete-user --email=' + email) + }) + + it('should have deleted the user on success', async function () { + const email = user.email + run('node delete-user --email=' + email) + const dbEntry = await user.get() + expect(dbEntry).to.not.exist + }) + + it('should exit with code 1 on missing email', function () { + try { + run('node delete-user') + } catch (e) { + expect(e.status).to.equal(1) + return + } + expect.fail('command should have failed') + }) + }) +})