feat: onboarding pre-work
This commit is contained in:
315
backend/src/_system/cli/create-admin.ts
Normal file
315
backend/src/_system/cli/create-admin.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* @ai-summary CLI script for creating initial admin user on fresh deployments
|
||||
* @ai-context Runs inside backend container with access to Auth0 and PostgreSQL
|
||||
*
|
||||
* Usage: node dist/_system/cli/create-admin.js
|
||||
*
|
||||
* Interactive prompts for email and password, then:
|
||||
* 1. Checks if any admin exists (errors if yes)
|
||||
* 2. Creates user in Auth0 via Management API
|
||||
* 3. Creates user_profile record
|
||||
* 4. Creates admin_users record with audit log
|
||||
*/
|
||||
|
||||
import * as readline from 'readline';
|
||||
import { Pool } from 'pg';
|
||||
import { appConfig } from '../../core/config/config-loader';
|
||||
import { auth0ManagementClient } from '../../core/auth/auth0-management.client';
|
||||
import { UserProfileRepository } from '../../features/user-profile/data/user-profile.repository';
|
||||
import { AdminRepository } from '../../features/admin/data/admin.repository';
|
||||
import { AdminService } from '../../features/admin/domain/admin.service';
|
||||
|
||||
// Terminal color codes
|
||||
const colors = {
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m',
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
};
|
||||
|
||||
function printInfo(msg: string): void {
|
||||
console.log(`${colors.green}[INFO]${colors.reset} ${msg}`);
|
||||
}
|
||||
|
||||
function printError(msg: string): void {
|
||||
console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`);
|
||||
}
|
||||
|
||||
function printWarn(msg: string): void {
|
||||
console.log(`${colors.yellow}[WARN]${colors.reset} ${msg}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for text input
|
||||
*/
|
||||
function prompt(rl: readline.Interface, question: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt for password input (masked - characters not echoed)
|
||||
*/
|
||||
function promptPassword(question: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
process.stdout.write(question);
|
||||
|
||||
const stdin = process.stdin;
|
||||
const wasRaw = stdin.isRaw;
|
||||
|
||||
stdin.setRawMode?.(true);
|
||||
stdin.resume();
|
||||
|
||||
let password = '';
|
||||
|
||||
const onData = (char: Buffer): void => {
|
||||
const c = char.toString();
|
||||
|
||||
if (c === '\n' || c === '\r') {
|
||||
stdin.removeListener('data', onData);
|
||||
stdin.setRawMode?.(wasRaw);
|
||||
stdin.pause();
|
||||
console.log(); // newline after password
|
||||
resolve(password);
|
||||
} else if (c === '\u0003') {
|
||||
// Ctrl+C - exit gracefully
|
||||
console.log('\n\nAborted by user.');
|
||||
process.exit(1);
|
||||
} else if (c === '\u007f' || c === '\b') {
|
||||
// Backspace
|
||||
password = password.slice(0, -1);
|
||||
// Optionally show backspace effect
|
||||
if (password.length >= 0) {
|
||||
process.stdout.write('\b \b');
|
||||
}
|
||||
} else if (c.charCodeAt(0) >= 32) {
|
||||
// Printable character
|
||||
password += c;
|
||||
process.stdout.write('*'); // Show asterisk for each character
|
||||
}
|
||||
};
|
||||
|
||||
stdin.on('data', onData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email format
|
||||
*/
|
||||
function validateEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate password strength
|
||||
* Returns error message or null if valid
|
||||
*/
|
||||
function validatePassword(password: string): string | null {
|
||||
if (password.length < 8) {
|
||||
return 'Password must be at least 8 characters long';
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
return 'Password must contain at least one uppercase letter';
|
||||
}
|
||||
if (!/[a-z]/.test(password)) {
|
||||
return 'Password must contain at least one lowercase letter';
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
return 'Password must contain at least one number';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for database to be available
|
||||
*/
|
||||
async function waitForDatabase(pool: Pool, timeoutMs = 30000): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (true) {
|
||||
try {
|
||||
await pool.query('SELECT 1');
|
||||
return;
|
||||
} catch (error) {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
throw new Error('Database connection timeout');
|
||||
}
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log('');
|
||||
console.log(`${colors.bold}${colors.cyan}========================================${colors.reset}`);
|
||||
console.log(`${colors.bold}${colors.cyan} MotoVaultPro - Create Admin User${colors.reset}`);
|
||||
console.log(`${colors.bold}${colors.cyan}========================================${colors.reset}`);
|
||||
console.log('');
|
||||
|
||||
// Initialize database pool
|
||||
const pool = new Pool({
|
||||
connectionString: appConfig.getDatabaseUrl(),
|
||||
});
|
||||
|
||||
try {
|
||||
// Wait for database
|
||||
printInfo('Connecting to database...');
|
||||
await waitForDatabase(pool);
|
||||
printInfo('Database connected.');
|
||||
|
||||
// Initialize repositories and services
|
||||
const userProfileRepo = new UserProfileRepository(pool);
|
||||
const adminRepo = new AdminRepository(pool);
|
||||
const adminService = new AdminService(adminRepo);
|
||||
|
||||
// Step 1: Check if any admin already exists
|
||||
printInfo('Checking for existing admin users...');
|
||||
const existingAdmins = await adminService.getActiveAdmins();
|
||||
|
||||
if (existingAdmins.length > 0) {
|
||||
console.log('');
|
||||
printError('An admin user already exists!');
|
||||
console.log('');
|
||||
console.log(` Found ${existingAdmins.length} existing admin(s):`);
|
||||
for (const admin of existingAdmins) {
|
||||
console.log(` - ${admin.email} (${admin.role})`);
|
||||
}
|
||||
console.log('');
|
||||
console.log(' This command is only for initial admin setup on fresh deployments.');
|
||||
console.log(' To create additional admins, use the admin panel.');
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
printInfo('No existing admins found. Proceeding with setup.');
|
||||
console.log('');
|
||||
|
||||
// Step 2: Create readline interface for interactive prompts
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
// Prompt for email
|
||||
let email: string;
|
||||
while (true) {
|
||||
email = await prompt(rl, 'Enter admin email: ');
|
||||
if (!email) {
|
||||
printError('Email is required');
|
||||
continue;
|
||||
}
|
||||
if (!validateEmail(email)) {
|
||||
printError('Please enter a valid email address');
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Close readline before password prompts (we handle raw input directly)
|
||||
rl.close();
|
||||
|
||||
// Prompt for password
|
||||
let password: string;
|
||||
while (true) {
|
||||
password = await promptPassword('Enter password: ');
|
||||
const passwordError = validatePassword(password);
|
||||
if (passwordError) {
|
||||
printError(passwordError);
|
||||
continue;
|
||||
}
|
||||
|
||||
const confirmPassword = await promptPassword('Confirm password: ');
|
||||
if (password !== confirmPassword) {
|
||||
printError('Passwords do not match');
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
printInfo('Creating admin user...');
|
||||
|
||||
// Step 3: Create user in Auth0 (with email pre-verified for trusted CLI-created admins)
|
||||
printInfo('Creating user in Auth0...');
|
||||
let auth0Sub: string;
|
||||
try {
|
||||
auth0Sub = await auth0ManagementClient.createUser({ email, password, emailVerified: true });
|
||||
printInfo(`Auth0 user created: ${auth0Sub}`);
|
||||
} catch (error) {
|
||||
printError(
|
||||
`Failed to create Auth0 user: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 4: Create user_profile record (with email verified for trusted CLI-created admins)
|
||||
printInfo('Creating user profile...');
|
||||
try {
|
||||
const displayName = email.split('@')[0]; // Use email prefix as display name
|
||||
await userProfileRepo.create(auth0Sub, email, displayName);
|
||||
// Mark email as verified in database (trusted admin created via CLI)
|
||||
await userProfileRepo.updateEmailVerified(auth0Sub, true);
|
||||
printInfo('User profile created (email verified).');
|
||||
} catch (error) {
|
||||
printError(
|
||||
`Failed to create user profile: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
// Attempt to clean up Auth0 user
|
||||
printWarn('Attempting to clean up Auth0 user...');
|
||||
try {
|
||||
await auth0ManagementClient.deleteUser(auth0Sub);
|
||||
printInfo('Auth0 user cleaned up.');
|
||||
} catch (cleanupError) {
|
||||
printError('Failed to clean up Auth0 user - manual cleanup may be required');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Step 5: Create admin_users record
|
||||
printInfo('Creating admin record...');
|
||||
try {
|
||||
const admin = await adminService.createAdmin(
|
||||
email,
|
||||
'admin',
|
||||
auth0Sub,
|
||||
'system-cli' // createdBy indicator for CLI-created admins
|
||||
);
|
||||
printInfo(`Admin record created: ${admin.email} (${admin.role})`);
|
||||
} catch (error) {
|
||||
printError(
|
||||
`Failed to create admin record: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
printWarn('User was created but admin privileges were not assigned.');
|
||||
printWarn('You may need to manually grant admin access via the database.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Success
|
||||
console.log('');
|
||||
console.log(`${colors.bold}${colors.green}========================================${colors.reset}`);
|
||||
console.log(`${colors.bold}${colors.green} Admin User Created Successfully!${colors.reset}`);
|
||||
console.log(`${colors.bold}${colors.green}========================================${colors.reset}`);
|
||||
console.log('');
|
||||
console.log(` Email: ${email}`);
|
||||
console.log(` Role: admin`);
|
||||
console.log(` Auth0 ID: ${auth0Sub}`);
|
||||
console.log('');
|
||||
console.log(' You can now log in at https://motovaultpro.com');
|
||||
console.log('');
|
||||
} catch (error) {
|
||||
printError(`Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Only run if executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
Reference in New Issue
Block a user