freeschema 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/freeschema.js CHANGED
@@ -6,7 +6,6 @@ const { spawnSync } = require('child_process');
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const https = require('https');
9
- const http = require('http');
10
9
  const crypto = require('crypto');
11
10
  const readline = require('readline');
12
11
 
@@ -123,6 +122,47 @@ function generateMosquittoPassword(username, password) {
123
122
  return `${username}:$7$${iterations}$${salt.toString('base64')}$${hash.toString('base64')}\n`;
124
123
  }
125
124
 
125
+ function waitForAccessControl(container) {
126
+ process.stdout.write(' Waiting for access-control-server');
127
+ for (let i = 0; i < 30; i++) {
128
+ const r = spawnSync('docker', [
129
+ 'exec', container, 'curl', '-sf', 'http://localhost:7000/health'
130
+ ], { stdio: 'pipe' });
131
+ if (r.status === 0) { process.stdout.write(' ready\n'); return true; }
132
+ process.stdout.write('.');
133
+ sleep(2000);
134
+ }
135
+ process.stdout.write(' timed out\n');
136
+ return false;
137
+ }
138
+
139
+ function setupSuperAdmin() {
140
+ const env = readEnv();
141
+ const adminId = parseInt(env.SUPER_ADMIN_ID || '100000016', 10);
142
+ const out = runComposeOutput(['ps', '--format', 'json']);
143
+ const containers = out.split('\n')
144
+ .map(l => { try { return JSON.parse(l); } catch { return null; } })
145
+ .filter(Boolean);
146
+ const ac = containers.find(c => c.Service === 'access-control-server');
147
+ if (!ac) return;
148
+ const container = ac.Name || ac.ID;
149
+
150
+ if (!waitForAccessControl(container)) return;
151
+
152
+ const result = spawnSync('docker', [
153
+ 'exec', container, 'curl', '-sf',
154
+ '-X', 'POST', 'http://localhost:7000/api/access/super-admin/concept',
155
+ '-H', 'Content-Type: application/json',
156
+ '-d', JSON.stringify({ ConceptId: adminId })
157
+ ], { stdio: ['inherit', 'pipe', 'pipe'] });
158
+
159
+ if (result.status === 0) {
160
+ console.log(` Super admin concept ${adminId} ✓`);
161
+ } else {
162
+ console.log(` Super admin already set or skipped`);
163
+ }
164
+ }
165
+
126
166
  function ensureMosquittoConfig(cwd) {
127
167
  const dir = (sub) => path.join(cwd, 'mosquitto', sub);
128
168
  ['config', 'data', 'log'].forEach(d => {
@@ -557,6 +597,9 @@ async function cmdStart() {
557
597
  console.log(' Seed applied ✓');
558
598
  }
559
599
 
600
+ console.log('\nFinalizing setup…');
601
+ setupSuperAdmin();
602
+
560
603
  console.log('\nFreeSchema is running.');
561
604
  console.log(' Status: freeschema status');
562
605
  console.log(' Logs: freeschema logs');
@@ -569,8 +612,21 @@ async function cmdStart() {
569
612
  if (isPlaceholder) {
570
613
  console.log('\n─────────────────────────────────────────');
571
614
  console.log(' CLIENT_SECRET is not configured.');
572
- console.log(' Setting up admin credentials now…');
615
+ console.log(' Setting up admin credentials…');
573
616
  console.log('─────────────────────────────────────────');
617
+
618
+ // Wait for MySQL to be ready before inserting credentials
619
+ try {
620
+ const container = getMysqlContainer();
621
+ const env = readEnv();
622
+ const dbUser = env.DB_USER || env.MYSQL_USER || 'freeschema';
623
+ const dbPass = env.DB_PASSWORD || env.MYSQL_PASSWORD || 'Freeschema@123';
624
+ waitForMySQL(container, dbUser, dbPass);
625
+ } catch (e) {
626
+ console.log(' (skipping credentials setup — MySQL not running)');
627
+ return;
628
+ }
629
+
574
630
  await cmdSetupCredentials();
575
631
  }
576
632
  }
@@ -716,126 +772,113 @@ function cmdDbRestore() {
716
772
 
717
773
  // ── setup-credentials ─────────────────────────────────────────────────────────
718
774
 
719
- function httpPost(url, body) {
720
- return new Promise((resolve, reject) => {
721
- const parsed = new URL(url);
722
- const payload = JSON.stringify(body);
723
- const opts = {
724
- hostname: parsed.hostname,
725
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
726
- path: parsed.pathname,
727
- method: 'POST',
728
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
729
- };
730
- const lib = parsed.protocol === 'https:' ? https : http;
731
- const req = lib.request(opts, res => {
732
- let data = '';
733
- res.on('data', chunk => { data += chunk; });
734
- res.on('end', () => {
735
- try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
736
- catch { resolve({ status: res.statusCode, body: data }); }
737
- });
738
- });
739
- req.on('error', reject);
740
- req.write(payload);
741
- req.end();
742
- });
775
+ function hashClientSecret(plainSecret) {
776
+ // Matches C# ClientSecretHasher: PBKDF2-HMAC-SHA256, 100000 iterations, 16-byte salt, 32-byte output
777
+ // Stored format: "{iterations}.{base64_salt}.{base64_hash}"
778
+ const iterations = 100_000;
779
+ const salt = crypto.randomBytes(16);
780
+ const hash = crypto.pbkdf2Sync(plainSecret, salt, iterations, 32, 'sha256');
781
+ return `${iterations}.${salt.toString('base64')}.${hash.toString('base64')}`;
743
782
  }
744
783
 
745
- function httpPostWithToken(url, token) {
746
- return new Promise((resolve, reject) => {
747
- const parsed = new URL(url);
748
- const opts = {
749
- hostname: parsed.hostname,
750
- port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
751
- path: parsed.pathname,
752
- method: 'POST',
753
- headers: { 'Authorization': `Bearer ${token}`, 'Content-Length': 0 },
754
- };
755
- const lib = parsed.protocol === 'https:' ? https : http;
756
- const req = lib.request(opts, res => {
757
- let data = '';
758
- res.on('data', chunk => { data += chunk; });
759
- res.on('end', () => {
760
- try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
761
- catch { resolve({ status: res.statusCode, body: data }); }
762
- });
763
- });
764
- req.on('error', reject);
765
- req.end();
766
- });
784
+ function runSql(container, dbName, sql) {
785
+ const env = readEnv();
786
+ const dbPass = env.MYSQL_ROOT_PASSWORD || ('Admin@' + (env.MYSQL_PASSWORD || 'Freeschema@123'));
787
+ const result = spawnSync(
788
+ 'docker',
789
+ ['exec', '-i', container, 'mysql', '-uroot', `-p${dbPass}`, dbName],
790
+ { input: sql, stdio: ['pipe', 'pipe', 'pipe'] }
791
+ );
792
+ if (result.error) throw new Error(`docker exec failed: ${result.error.message}`);
793
+ if (result.status !== 0) throw new Error((result.stderr || '').toString().trim());
794
+ return (result.stdout || '').toString().trim();
767
795
  }
768
796
 
769
797
  async function cmdSetupCredentials() {
770
798
  const envPath = path.join(process.cwd(), '.env');
771
799
  if (!fs.existsSync(envPath)) die('.env not found. Run `freeschema init` first.');
772
800
 
773
- const env = readEnv();
774
- const wicoPort = env.WICO_PORT || '7071';
775
- const baseUrl = `http://localhost:${wicoPort}/api`;
776
-
777
- console.log(`\nSetting up CLIENT_ID and CLIENT_SECRET`);
778
- console.log(`Using WICO server at ${baseUrl}\n`);
801
+ const env = readEnv();
802
+ const dbName = env.DB_DATABASE || env.MYSQL_DATABASE || 'freeschema_db';
779
803
 
780
- // Step 1: login
781
- const email = await prompt('Admin email');
782
- const password = await promptSecret('Admin password');
783
- if (!email || !password) die('Email and password are required.');
804
+ console.log('\nSetting up CLIENT_ID and CLIENT_SECRET');
784
805
 
785
- process.stdout.write('\n[1/3] Logging in… ');
786
- let loginRes;
787
- try {
788
- loginRes = await httpPost(`${baseUrl}/auth/login`, { email, password, application: 'boomconsole' });
789
- } catch (e) {
790
- die(`Could not reach WICO server at ${baseUrl}/auth/login\nIs FreeSchema running? Try: freeschema start`);
791
- }
806
+ // Prompt for entity concept ID
807
+ const entityId = await prompt('Admin entity concept ID', env.CLIENT_ID || '100000016');
808
+ if (!entityId) die('Entity ID is required.');
792
809
 
793
- if (loginRes.status !== 200 || !loginRes.body) {
794
- die(`Login failed (HTTP ${loginRes.status}): ${JSON.stringify(loginRes.body)}`);
795
- }
810
+ // Step 1: generate secret + hash it locally — no API call needed
811
+ process.stdout.write('[1/3] Generating secret… ');
812
+ const plainSecret = crypto.randomBytes(32).toString('hex'); // 64-char hex
813
+ const hashedSecret = hashClientSecret(plainSecret);
814
+ console.log('done');
796
815
 
797
- const token = loginRes.body.token || loginRes.body.access_token || (loginRes.body.data && loginRes.body.data.token);
798
- if (!token) die(`No token in login response: ${JSON.stringify(loginRes.body)}`);
816
+ // Step 2: insert hash directly into the database
817
+ process.stdout.write('[2/3] Writing to database… ');
818
+ const container = getMysqlContainer();
799
819
 
800
- // Decode entity concept ID from JWT upn claim
801
- let entityId;
802
- try {
803
- const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
804
- entityId = payload.upn || payload.sub;
805
- } catch {
806
- die('Could not decode JWT token.');
807
- }
808
- if (!entityId) die('Could not determine entity ID from JWT.');
809
- console.log(`done (entity ${entityId})`);
820
+ const sql = `
821
+ SET @entity_id = ${parseInt(entityId, 10)};
822
+
823
+ -- Look up the type concept for 'the_client_secret'
824
+ SET @type_id = (
825
+ SELECT id FROM the_concepts
826
+ WHERE character_value = 'the_client_secret'
827
+ ORDER BY id ASC LIMIT 1
828
+ );
829
+
830
+ -- Insert the secret hash as a new concept
831
+ INSERT INTO the_concepts (
832
+ user_id, category_id, category_user_id, type_id, type_user_id,
833
+ character_value, security_id, security_user_id, access_id, access_user_id,
834
+ session_information_id, session_information_user_id
835
+ ) VALUES (
836
+ 999, @type_id, 999, @type_id, 999,
837
+ '${hashedSecret}', 4, 999, 4, 999, 999, 999
838
+ );
839
+
840
+ SET @secret_concept_id = LAST_INSERT_ID();
841
+
842
+ -- Look up the connection type for 'the_entity_s_client_secret'
843
+ SET @conn_type_id = (
844
+ SELECT id FROM the_concepts
845
+ WHERE character_value = 'the_entity_s_client_secret'
846
+ ORDER BY id ASC LIMIT 1
847
+ );
848
+
849
+ -- Link entity → secret concept
850
+ INSERT INTO the_connections (
851
+ user_id, of_the_concepts_id, of_the_concepts_user_id,
852
+ type_id, type_user_id, order_id, order_user_id,
853
+ to_the_concepts_id, to_the_concepts_user_id,
854
+ security_id, security_user_id, access_id, access_user_id,
855
+ session_information_id, session_information_user_id
856
+ ) VALUES (
857
+ 999, @entity_id, 999,
858
+ @conn_type_id, 999, 999, 999,
859
+ @secret_concept_id, 999,
860
+ 4, 999, 4, 999, 999, 999
861
+ );
862
+ `;
810
863
 
811
- // Step 2: generate client-secret
812
- process.stdout.write('[2/3] Generating client-secret… ');
813
- let secretRes;
814
864
  try {
815
- secretRes = await httpPostWithToken(`${baseUrl}/client-secret`, token);
865
+ runSql(container, dbName, sql);
816
866
  } catch (e) {
817
- die(`Request to /api/client-secret failed: ${e.message}`);
867
+ die(`Database insert failed: ${e.message}`);
818
868
  }
819
-
820
- if (secretRes.status !== 200 || !secretRes.body) {
821
- die(`client-secret failed (HTTP ${secretRes.status}): ${JSON.stringify(secretRes.body)}`);
822
- }
823
-
824
- const secret = secretRes.body.message || secretRes.body.data || secretRes.body;
825
- if (!secret || typeof secret !== 'string') die(`Empty secret in response: ${JSON.stringify(secretRes.body)}`);
826
869
  console.log('done');
827
870
 
828
871
  // Step 3: update .env
829
872
  process.stdout.write('[3/3] Updating .env… ');
830
873
  setEnvVar(envPath, 'CLIENT_ID', String(entityId));
831
- setEnvVar(envPath, 'CLIENT_SECRET', secret);
874
+ setEnvVar(envPath, 'CLIENT_SECRET', plainSecret);
832
875
  console.log('done');
833
876
 
834
877
  console.log(`
835
878
  ─────────────────────────────────────────
836
879
  Credentials saved to .env:
837
880
  CLIENT_ID = ${entityId}
838
- CLIENT_SECRET = ${secret}
881
+ CLIENT_SECRET = ${plainSecret}
839
882
  ─────────────────────────────────────────
840
883
 
841
884
  Restart the node-server to apply:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freeschema",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "FreeSchema — self-hosted deployment CLI",
5
5
  "keywords": [
6
6
  "freeschema",
@@ -121,3 +121,8 @@ DISABLE_SWAGGER=true
121
121
  # ====================================
122
122
  CLIENT_ID=
123
123
  CLIENT_SECRET=
124
+
125
+ # ====================================
126
+ # SUPER ADMIN SETUP
127
+ # ====================================
128
+ SUPER_ADMIN_ID=100000016
@@ -188,6 +188,12 @@ services:
188
188
  condition: service_started
189
189
  networks:
190
190
  - freeschema-network
191
+ healthcheck:
192
+ test: ["CMD-SHELL", "curl -sI http://localhost:5000/health || exit 1"]
193
+ interval: 1m30s
194
+ timeout: 30s
195
+ retries: 5
196
+ start_period: 30s
191
197
 
192
198
  log-server:
193
199
  image: mentorayush/log-server:latest
@@ -229,6 +235,12 @@ services:
229
235
  - node-server
230
236
  networks:
231
237
  - freeschema-network
238
+ healthcheck:
239
+ test: ["CMD-SHELL", "wget -q --spider http://localhost/ || exit 1"]
240
+ interval: 30s
241
+ timeout: 10s
242
+ retries: 3
243
+ start_period: 30s
232
244
 
233
245
  wico-app:
234
246
  image: mentorayush/wico-app:latest
@@ -243,6 +255,12 @@ services:
243
255
  - node-server
244
256
  networks:
245
257
  - freeschema-network
258
+ healthcheck:
259
+ test: ["CMD-SHELL", "wget -q --spider http://localhost/ || exit 1"]
260
+ interval: 30s
261
+ timeout: 10s
262
+ retries: 3
263
+ start_period: 30s
246
264
 
247
265
  nginx-server:
248
266
  image: nginx:alpine
@@ -255,13 +273,20 @@ services:
255
273
  volumes:
256
274
  - ./nginx/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
257
275
  depends_on:
258
- - wico-app
259
- - vccs-app
260
- - node-server
261
- - node-cache-server
262
- - log-server
263
- - wico-server
264
- - access-control-server
276
+ access-control-server:
277
+ condition: service_healthy
278
+ wico-server:
279
+ condition: service_healthy
280
+ node-server:
281
+ condition: service_healthy
282
+ node-cache-server:
283
+ condition: service_healthy
284
+ log-server:
285
+ condition: service_healthy
286
+ vccs-app:
287
+ condition: service_healthy
288
+ wico-app:
289
+ condition: service_healthy
265
290
  networks:
266
291
  - freeschema-network
267
292