EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
init_root_tenant.js
Go to the documentation of this file.
1#!/usr/bin/env node
2// Initialize root tenant and admin user without psql (uses node-postgres and bcrypt)
3// Usage:
4// node init_root_tenant.js --email admin@example.com [--tenant "Root MSP"] [--subdomain root] [--name "System Administrator"] [--password "..."]
5// DATABASE_URL is read from devops/digitalocean/db_credentials.txt or env
6
7const fs = require('fs');
8const path = require('path');
9const { Pool } = require('pg');
10const bcrypt = require('bcrypt');
11const crypto = require('crypto');
12
13const args = process.argv.slice(2);
14function getArg(flag, def) {
15 const idx = args.indexOf(flag);
16 if (idx !== -1 && args[idx + 1]) return args[idx + 1];
17 return def;
18}
19
20const email = getArg('--email');
21if (!email) {
22 console.error('Usage: node init_root_tenant.js --email admin@example.com [--tenant "Root MSP"] [--subdomain root] [--name "System Administrator"] [--password "..."]');
23 process.exit(1);
24}
25
26const tenantName = getArg('--tenant', 'Root MSP');
27const subdomain = getArg('--subdomain', 'root');
28const adminName = getArg('--name', 'System Administrator');
29let adminPassword = getArg('--password');
30
31if (!adminPassword) {
32 adminPassword = crypto.randomBytes(12).toString('base64').replace(/[^a-zA-Z0-9]/g, '').slice(0, 16);
33}
34
35const ROOT = path.resolve(__dirname, '../..');
36const CREDS_FILE = path.join(ROOT, 'devops', 'digitalocean', 'db_credentials.txt');
37
38function loadDatabaseUrlFromCreds() {
39 if (!fs.existsSync(CREDS_FILE)) return null;
40 const content = fs.readFileSync(CREDS_FILE, 'utf8');
41 const match = content.match(/^DATABASE_URL=(.*)$/m);
42 return match ? match[1].trim() : null;
43}
44
45(async () => {
46 let databaseUrl = loadDatabaseUrlFromCreds() || process.env.DATABASE_URL;
47 if (!databaseUrl) {
48 console.error('[ERR] No DATABASE_URL found in creds/env.');
49 process.exit(1);
50 }
51
52 // Strip sslmode=require; set SSL in client options
53 databaseUrl = databaseUrl.replace(/([?&])sslmode=require(&|$)/, (m, p1, p2) => (p2 === '&' ? p1 : ''));
54 const pool = new Pool({ connectionString: databaseUrl, ssl: { rejectUnauthorized: false } });
55
56 try {
57 await pool.query('BEGIN');
58
59 // Upsert tenant
60 const tenantRes = await pool.query(
61 `INSERT INTO tenants (name, subdomain, is_msp, status)
62 VALUES ($1, $2, TRUE, 'active')
63 ON CONFLICT (subdomain) DO UPDATE
64 SET name = EXCLUDED.name,
65 is_msp = TRUE,
66 status = 'active',
67 updated_at = NOW()
68 RETURNING tenant_id`,
69 [tenantName, subdomain]
70 );
71 const tenantId = tenantRes.rows[0].tenant_id;
72
73 // Hash password
74 const hash = await bcrypt.hash(adminPassword, 10);
75
76 // Ensure users table has required columns; fallback between name/full_name
77 // Prefer columns: (tenant_id, email, password_hash, full_name, role, is_active)
78 // Fallback to (tenant_id, email, password_hash, name, role)
79
80 // Detect columns quickly
81 const colsRes = await pool.query(`SELECT column_name FROM information_schema.columns WHERE table_name='users'`);
82 const cols = colsRes.rows.map(r => r.column_name);
83 const hasFullName = cols.includes('full_name');
84 const hasIsActive = cols.includes('is_active');
85
86 if (hasFullName) {
87 await pool.query(
88 `INSERT INTO users (tenant_id, email, password_hash, full_name, role${hasIsActive ? ', is_active' : ''})
89 VALUES ($1, $2, $3, $4, 'admin'${hasIsActive ? ', TRUE' : ''})
90 ON CONFLICT (email) DO UPDATE
91 SET password_hash = EXCLUDED.password_hash,
92 full_name = EXCLUDED.full_name,
93 role = 'admin',
94 ${hasIsActive ? 'is_active = TRUE,' : ''}
95 updated_at = NOW()`,
96 [tenantId, email, hash, adminName]
97 );
98 } else {
99 await pool.query(
100 `INSERT INTO users (tenant_id, email, password_hash, name, role)
101 VALUES ($1, $2, $3, $4, 'admin')
102 ON CONFLICT (email) DO UPDATE
103 SET password_hash = EXCLUDED.password_hash,
104 name = EXCLUDED.name,
105 role = 'admin'`,
106 [tenantId, email, hash, adminName]
107 );
108 }
109
110 await pool.query('COMMIT');
111
112 // Save credentials to secure file
113 const outFile = path.join(ROOT, 'devops', 'digitalocean', 'admin_credentials.txt');
114 const contents = `# Root Admin Credentials\n# Created: ${new Date().toISOString()}\n\nTENANT_NAME=${tenantName}\nSUBDOMAIN=${subdomain}\nADMIN_EMAIL=${email}\nADMIN_NAME=${adminName}\nADMIN_PASSWORD=${adminPassword}\n\nLogin URL: https://demo.everydaytech.au/login\n`;
115 fs.writeFileSync(outFile, contents, { mode: 0o600 });
116
117 console.log('==========================================');
118 console.log('✅ Root Tenant & Admin Initialized');
119 console.log('==========================================');
120 console.log(`Tenant: ${tenantName} (subdomain: ${subdomain})`);
121 console.log(`Admin: ${adminName} <${email}>`);
122 console.log('Credentials saved to devops/digitalocean/admin_credentials.txt (chmod 600)');
123 } catch (err) {
124 await pool.query('ROLLBACK');
125 console.error('[ERR] Initialization failed:', err.message);
126 process.exit(2);
127 } finally {
128 await pool.end();
129 }
130})();