feat: onboarding pre-work

This commit is contained in:
Eric Gullickson
2025-12-22 21:34:05 -06:00
parent 4897f0a52c
commit 55cf4923b8
12 changed files with 537 additions and 71 deletions

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