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.
This commit is contained in:
Daniel Hiller
2026-09-17 00:48:08 +02:00
parent 84827ab354
commit 8236cde954
+19 -17
View File
@@ -62,34 +62,36 @@ exports.seed = async (knex) => {
const defaultAdminEmail =
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.`,
// 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 (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.`);
}
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) {
throw new Error(`User with DEFAULT_ADMIN_USERNAME "${userData.username}" already exists.`);
console.warn(
`Warning: DEFAULT_ADMIN_USERNAME "${userData.username}" belongs to another user; leaving the username unchanged.`,
);
delete userData.username;
}
}