diff --git a/server/db/create-admin-user.js b/server/db/create-admin-user.js index a41fa5e2..4edc8f70 100644 --- a/server/db/create-admin-user.js +++ b/server/db/create-admin-user.js @@ -7,6 +7,8 @@ /* eslint-disable no-console */ const { read } = require('read'); +const validator = require('validator'); +const zxcvbn = require('zxcvbn'); const initKnex = require('knex'); const knexfile = require('./knexfile'); @@ -47,20 +49,86 @@ const input = async (fieldName, options = {}) => { try { await knex.migrate.latest(); - process.env.DEFAULT_ADMIN_EMAIL = await input('Email', { - isRequired: true, - }); + let isEmailValid = false; + while (!isEmailValid) { + process.env.DEFAULT_ADMIN_EMAIL = await input('Email', { + isRequired: true, + }); - process.env.DEFAULT_ADMIN_PASSWORD = await input('Password', { - isRequired: true, - isPassword: true, - }); + if ( + !validator.isEmail(process.env.DEFAULT_ADMIN_EMAIL) || + process.env.DEFAULT_ADMIN_EMAIL.length > 256 + ) { + console.log('Email must be a valid e-mail address with no more than 256 characters.'); + } else { + const existingUser = await knex('user_account') + .where('email', process.env.DEFAULT_ADMIN_EMAIL.toLowerCase()) + .first(); + + if (existingUser) { + console.log('Email is already in use.'); + } else { + isEmailValid = true; + } + } + } + + let isPasswordValid = false; + while (!isPasswordValid) { + process.env.DEFAULT_ADMIN_PASSWORD = await input('Password', { + isRequired: true, + isPassword: true, + }); + + if (zxcvbn(process.env.DEFAULT_ADMIN_PASSWORD).score >= 2) { + isPasswordValid = true; + } else { + console.log('Password is too weak. Choose a stronger password.'); + } + } process.env.DEFAULT_ADMIN_NAME = await input('Name', { isRequired: true, }); - process.env.DEFAULT_ADMIN_USERNAME = await input('Username'); + while (process.env.DEFAULT_ADMIN_NAME.length > 128) { + console.log('Name must be no more than 128 characters.'); + process.env.DEFAULT_ADMIN_NAME = await input('Name', { + isRequired: true, + }); + } + + const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/; + + let isUsernameValid = false; + while (!isUsernameValid) { + process.env.DEFAULT_ADMIN_USERNAME = await input('Username'); + + if (!process.env.DEFAULT_ADMIN_USERNAME) { + break; + } + + const isValid = + process.env.DEFAULT_ADMIN_USERNAME.length >= 3 && + process.env.DEFAULT_ADMIN_USERNAME.length <= 32 && + USERNAME_REGEX.test(process.env.DEFAULT_ADMIN_USERNAME); + + if (!isValid) { + console.log( + 'Username must be 3-32 characters and contain only letters, digits, underscores, and dots (e.g., john_doe).', + ); + } else { + const existingUser = await knex('user_account') + .where('username', process.env.DEFAULT_ADMIN_USERNAME.toLowerCase()) + .first(); + + if (existingUser) { + console.log('Username is already in use.'); + } else { + isUsernameValid = true; + } + } + } await knex.seed.run({ specific: 'default.js', diff --git a/server/db/seeds/default.js b/server/db/seeds/default.js index 6a92ae90..82828cdf 100644 --- a/server/db/seeds/default.js +++ b/server/db/seeds/default.js @@ -3,7 +3,12 @@ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md */ +/* eslint-disable no-console */ + const bcrypt = require('bcrypt'); +const validator = require('validator'); + +const USERNAME_REGEX = /^[a-zA-Z0-9]+((_|\.)?[a-zA-Z0-9])*$/; const buildUserData = () => { const data = { @@ -15,10 +20,23 @@ const buildUserData = () => { data.password = bcrypt.hashSync(process.env.DEFAULT_ADMIN_PASSWORD, 10); } if (process.env.DEFAULT_ADMIN_NAME) { - data.name = process.env.DEFAULT_ADMIN_NAME; + if (process.env.DEFAULT_ADMIN_NAME.length > 128) { + console.warn('Warning: DEFAULT_ADMIN_NAME exceeds 128 characters; truncating.'); + data.name = process.env.DEFAULT_ADMIN_NAME.slice(0, 128); + } else { + data.name = process.env.DEFAULT_ADMIN_NAME; + } } if (process.env.DEFAULT_ADMIN_USERNAME) { - data.username = process.env.DEFAULT_ADMIN_USERNAME.toLowerCase(); + const username = process.env.DEFAULT_ADMIN_USERNAME.toLowerCase(); + + if (username.length < 3 || username.length > 32 || !USERNAME_REGEX.test(username)) { + console.warn( + `Warning: DEFAULT_ADMIN_USERNAME "${process.env.DEFAULT_ADMIN_USERNAME}" is invalid; skipping.`, + ); + } else { + data.username = username; + } } return data; @@ -45,8 +63,36 @@ exports.seed = async (knex) => { process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(); if (defaultAdminEmail) { + if (!validator.isEmail(defaultAdminEmail)) { + throw new Error( + `DEFAULT_ADMIN_EMAIL "${process.env.DEFAULT_ADMIN_EMAIL}" is not a valid e-mail address.`, + ); + } + + if (defaultAdminEmail.length > 256) { + throw new Error( + `DEFAULT_ADMIN_EMAIL "${process.env.DEFAULT_ADMIN_EMAIL}" exceeds 256 characters.`, + ); + } + + const existingEmailUser = await knex('user_account').where('email', defaultAdminEmail).first(); + + if (existingEmailUser) { + throw new Error(`User with DEFAULT_ADMIN_EMAIL "${defaultAdminEmail}" already exists.`); + } + const userData = buildUserData(); + if (userData.username) { + const existingUsernameUser = await knex('user_account') + .where('username', userData.username) + .first(); + + if (existingUsernameUser) { + throw new Error(`User with DEFAULT_ADMIN_USERNAME "${userData.username}" already exists.`); + } + } + let userId; try { [{ id: userId }] = await knex('user_account').insert(