Fix initial user creating

This commit is contained in:
2026-04-29 00:55:00 +03:00
parent 3565390a49
commit 66c84ecdeb
16 changed files with 267 additions and 35 deletions

View File

@@ -5,11 +5,13 @@ const compression = require('compression');
require('dotenv').config();
const logger = require('./config/logger');
const pool = require('./config/database');
const apiRoutes = require('./routes/api');
const { authenticateApiKey } = require('./middleware/auth');
const createRateLimiter = require('./middleware/rateLimiter');
const { ensureRealtimeSchema } = require('./services/realtimeSchema');
const { attachRealtimeServer, closeRealtimeServer } = require('./services/realtimeService');
const { ensureBootstrapAdmin } = require('./utils/bootstrapAdmin');
const app = express();
const PORT = process.env.PORT || 3000;
@@ -133,9 +135,31 @@ function shutdown(signal) {
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForDatabase({ attempts = 30, delayMs = 1000 } = {}) {
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
await pool.query('SELECT 1');
return;
} catch (error) {
if (attempt === attempts) {
throw error;
}
logger.warn(`Database is not ready yet (${attempt}/${attempts}); retrying...`);
await sleep(delayMs);
}
}
}
async function startServer() {
try {
await waitForDatabase();
await ensureRealtimeSchema();
await ensureBootstrapAdmin();
server = app.listen(PORT, () => {
logger.info(`Server is running on port ${PORT}`);

122
backend/src/utils/bootstrapAdmin.js vendored Normal file
View File

@@ -0,0 +1,122 @@
const bcrypt = require('bcrypt');
const pool = require('../config/database');
const logger = require('../config/logger');
const DEFAULT_ADMIN = {
username: 'admin',
password: 'admin123',
fullName: 'Main administrator',
email: 'admin@polotsk-transit.local',
};
function readBoolean(value, defaultValue) {
if (value === undefined || value === null || value === '') {
return defaultValue;
}
return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
}
function readBootstrapAdminConfig(env = process.env) {
return {
enabled: readBoolean(env.BOOTSTRAP_ADMIN, true),
username: env.ADMIN_USERNAME || DEFAULT_ADMIN.username,
password: env.ADMIN_PASSWORD || DEFAULT_ADMIN.password,
fullName: env.ADMIN_FULL_NAME || DEFAULT_ADMIN.fullName,
email: env.ADMIN_EMAIL || DEFAULT_ADMIN.email,
resetPassword: readBoolean(env.BOOTSTRAP_ADMIN_RESET_PASSWORD, false),
};
}
async function activeAdminExists() {
const result = await pool.query(`
SELECT EXISTS (
SELECT 1
FROM users
WHERE role = 'admin' AND is_active = true
) AS exists
`);
return result.rows[0]?.exists === true;
}
function validateConfig(config) {
if (!config.username) {
throw new Error('ADMIN_USERNAME cannot be empty');
}
if (!config.password || config.password.length < 6) {
throw new Error('ADMIN_PASSWORD must be at least 6 characters');
}
}
async function ensureBootstrapAdmin(options = {}) {
const config = {
...readBootstrapAdminConfig(),
...options,
};
if (!config.enabled) {
logger.info('Bootstrap admin is disabled');
return { changed: false, reason: 'disabled' };
}
validateConfig(config);
const existingResult = await pool.query(
'SELECT id, username, role, is_active FROM users WHERE username = $1',
[config.username]
);
const existingUser = existingResult.rows[0];
if (existingUser) {
const needsRepair = existingUser.role !== 'admin' || existingUser.is_active !== true;
const shouldResetPassword = config.resetPassword || needsRepair;
if (!needsRepair && !shouldResetPassword) {
logger.info(`Bootstrap admin already exists: ${config.username}`);
return { changed: false, user: existingUser };
}
const passwordHash = shouldResetPassword
? await bcrypt.hash(config.password, 10)
: null;
const updateResult = await pool.query(`
UPDATE users
SET role = 'admin',
is_active = true,
full_name = COALESCE($2, full_name),
email = COALESCE($3, email),
password_hash = COALESCE($4, password_hash)
WHERE username = $1
RETURNING id, username, role, is_active
`, [config.username, config.fullName || null, config.email || null, passwordHash]);
logger.info(`Bootstrap admin repaired: ${config.username}`);
return { changed: true, user: updateResult.rows[0] };
}
const shouldCreateWithExistingAdmin = options.createIfActiveAdminExists === true;
if (!shouldCreateWithExistingAdmin && await activeAdminExists()) {
logger.info('Active admin already exists; default bootstrap admin was not created');
return { changed: false, reason: 'active-admin-exists' };
}
const passwordHash = await bcrypt.hash(config.password, 10);
const insertResult = await pool.query(`
INSERT INTO users (username, password_hash, full_name, email, role, is_active)
VALUES ($1, $2, $3, $4, 'admin', true)
RETURNING id, username, role, is_active
`, [config.username, passwordHash, config.fullName || null, config.email || null]);
logger.info(`Bootstrap admin created: ${config.username}`);
return { changed: true, user: insertResult.rows[0] };
}
module.exports = {
DEFAULT_ADMIN,
ensureBootstrapAdmin,
readBootstrapAdminConfig,
};