The validation added with the admin user checks reached further than intended. The seed runs on every container start, not only on the first, and it threw on three conditions that are normal on a second start. The worst was the check for an existing user: after the first start the default admin exists by definition, so every restart after that ended in a container that would not come up. The insert is wrapped in a try and falls back to an update precisely because it is meant to run again, and that path was never reached. The other two were narrower but the same shape. `validator.isEmail` rejects `admin@localhost`, which is a common value in self-hosted setups, so an instance that had been running for a year would stop coming up after the upgrade. None of it throws now. Bad input is reported on stderr and skipped, the way an invalid username already was, and a username that belongs to someone else is left alone rather than taken. `db:create-admin-user` keeps the strict validation, which is where a person is there to read it and type again.
167 lines
5.0 KiB
JavaScript
167 lines
5.0 KiB
JavaScript
/*!
|
|
* Copyright (c) 2024 PLANKA Software GmbH
|
|
* 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 = {
|
|
role: 'admin',
|
|
isDeactivated: false,
|
|
};
|
|
|
|
if (process.env.DEFAULT_ADMIN_PASSWORD) {
|
|
data.password = bcrypt.hashSync(process.env.DEFAULT_ADMIN_PASSWORD, 10);
|
|
}
|
|
if (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) {
|
|
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;
|
|
};
|
|
|
|
const buildInternalConfigData = () => {
|
|
const data = {};
|
|
if (process.env.STORAGE_LIMIT) {
|
|
data.storageLimit = process.env.STORAGE_LIMIT;
|
|
}
|
|
if (process.env.ACTIVE_USERS_LIMIT) {
|
|
const activeUsersLimit = parseInt(process.env.ACTIVE_USERS_LIMIT, 10);
|
|
|
|
if (Number.isInteger(activeUsersLimit)) {
|
|
data.activeUsersLimit = activeUsersLimit;
|
|
}
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
exports.seed = async (knex) => {
|
|
const defaultAdminEmail =
|
|
process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase();
|
|
|
|
// Nothing in here may throw. The seed runs on every start, not only on the
|
|
// first one, and a throw here leaves the container in a restart loop with an
|
|
// instance that used to come up fine. Bad input is reported and skipped; the
|
|
// interactive `db:create-admin-user` is where invalid input is refused.
|
|
const isDefaultAdminEmailUsable =
|
|
defaultAdminEmail && validator.isEmail(defaultAdminEmail) && defaultAdminEmail.length <= 256;
|
|
|
|
if (defaultAdminEmail && !isDefaultAdminEmailUsable) {
|
|
console.warn(
|
|
`Warning: DEFAULT_ADMIN_EMAIL "${process.env.DEFAULT_ADMIN_EMAIL}" is not a usable e-mail address; skipping the default admin user.`,
|
|
);
|
|
}
|
|
|
|
if (isDefaultAdminEmailUsable) {
|
|
const userData = buildUserData();
|
|
|
|
if (userData.username) {
|
|
const existingUsernameUser = await knex('user_account')
|
|
.where('username', userData.username)
|
|
.whereNot('email', defaultAdminEmail)
|
|
.first();
|
|
|
|
// Taken by somebody else, so it cannot be applied. The account itself is
|
|
// still created or refreshed, it simply keeps the username it has.
|
|
if (existingUsernameUser) {
|
|
console.warn(
|
|
`Warning: DEFAULT_ADMIN_USERNAME "${userData.username}" belongs to another user; leaving the username unchanged.`,
|
|
);
|
|
|
|
delete userData.username;
|
|
}
|
|
}
|
|
|
|
let userId;
|
|
try {
|
|
[{ id: userId }] = await knex('user_account').insert(
|
|
{
|
|
...userData,
|
|
email: defaultAdminEmail,
|
|
subscribeToOwnCards: false,
|
|
subscribeToCardWhenCommenting: true,
|
|
turnOffRecentCardHighlighting: false,
|
|
enableFavoritesByDefault: true,
|
|
defaultEditorMode: 'wysiwyg',
|
|
defaultHomeView: 'groupedProjects',
|
|
defaultProjectsOrder: 'byDefault',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
'id',
|
|
);
|
|
} catch (error) {
|
|
/* empty */
|
|
}
|
|
|
|
if (!userId) {
|
|
await knex('user_account').update(userData).where('email', defaultAdminEmail);
|
|
}
|
|
}
|
|
|
|
const internalConfigData = buildInternalConfigData();
|
|
|
|
let activeUsersLimit;
|
|
if (Object.keys(internalConfigData).length > 0) {
|
|
[{ active_users_limit: activeUsersLimit }] = await knex('internal_config')
|
|
.update(internalConfigData)
|
|
.returning('active_users_limit');
|
|
} else {
|
|
({ active_users_limit: activeUsersLimit } = await knex('internal_config')
|
|
.select('active_users_limit')
|
|
.first());
|
|
}
|
|
|
|
if (Number.isInteger(activeUsersLimit)) {
|
|
let orderByQuery;
|
|
let orderByQueryValues;
|
|
|
|
if (defaultAdminEmail) {
|
|
orderByQuery = 'CASE WHEN email = ? THEN 0 WHEN role = ? THEN 1 ELSE 2 END';
|
|
orderByQueryValues = [defaultAdminEmail, 'admin'];
|
|
} else {
|
|
orderByQuery = 'CASE WHEN role = ? THEN 0 ELSE 1 END';
|
|
orderByQueryValues = 'admin';
|
|
}
|
|
|
|
const users = await knex('user_account')
|
|
.select('id')
|
|
.where('is_deactivated', false)
|
|
.orderByRaw(orderByQuery, orderByQueryValues)
|
|
.orderBy('id')
|
|
.offset(activeUsersLimit);
|
|
|
|
if (users.length > 0) {
|
|
const userIds = users.map(({ id }) => id);
|
|
|
|
await knex('user_account')
|
|
.update({
|
|
isDeactivated: true,
|
|
})
|
|
.whereIn('id', userIds);
|
|
}
|
|
}
|
|
};
|