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 +137 -94
- package/package.json +1 -1
- package/templates/.env.example +5 -0
- package/templates/docker-compose.yml +32 -7
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
|
|
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
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
|
774
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
794
|
-
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
865
|
+
runSql(container, dbName, sql);
|
|
816
866
|
} catch (e) {
|
|
817
|
-
die(`
|
|
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',
|
|
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 = ${
|
|
881
|
+
CLIENT_SECRET = ${plainSecret}
|
|
839
882
|
─────────────────────────────────────────
|
|
840
883
|
|
|
841
884
|
Restart the node-server to apply:
|
package/package.json
CHANGED
package/templates/.env.example
CHANGED
|
@@ -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
|
-
-
|
|
259
|
-
|
|
260
|
-
-
|
|
261
|
-
|
|
262
|
-
-
|
|
263
|
-
|
|
264
|
-
-
|
|
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
|
|