Fix initial user creating
This commit is contained in:
@@ -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
122
backend/src/utils/bootstrapAdmin.js
vendored
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user