Files
planka/server/db/seeds/default.js
T
Daniel Hiller 8236cde954 fix: Keep the seed from refusing to start an instance that already ran
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.
2026-09-17 00:48:08 +02:00

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);
}
}
};