@restforgejs/platform 5.2.16 → 5.3.5
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/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/endpoint/create.js +69 -6
- package/generators/cli/payload/sync.js +16 -6
- package/generators/cli/project/auth.js +2 -2
- package/generators/cli/project/sdk.js +112 -0
- package/generators/lib/arg-parser.js +6 -0
- package/generators/lib/auth/processor-generator.js +5 -3
- package/generators/lib/auth/templates/processor/google.js.tmpl +178 -0
- package/generators/lib/auth/templates/processor/login.js.tmpl +8 -8
- package/generators/lib/auth/templates/processor/logout.js.tmpl +2 -2
- package/generators/lib/auth/templates/processor/me.js.tmpl +2 -2
- package/generators/lib/auth/templates/processor/refresh.js.tmpl +6 -6
- package/generators/lib/auth/templates/processor/register.js.tmpl +4 -4
- package/generators/lib/auth/templates/processor/reset-password.js.tmpl +7 -7
- package/generators/lib/auth/templates/rfx_auth.js.tmpl +3 -0
- package/generators/lib/generators/model-generator.js +46 -59
- package/generators/lib/help-generator.js +41 -3
- package/generators/lib/payload/endpoint-schema-validator.js +8 -3
- package/generators/lib/payload/field-projections.js +116 -0
- package/generators/lib/payload/payload-runner.js +164 -48
- package/generators/lib/payload/schema-diff.js +108 -0
- package/generators/lib/sdk/generator.js +719 -0
- package/generators/lib/sdk/naming.js +48 -0
- package/generators/lib/sdk/runtime/README.md.tmpl +207 -0
- package/generators/lib/sdk/runtime/auth-client.js +186 -0
- package/generators/lib/sdk/runtime/deploy.mjs.tmpl +85 -0
- package/generators/lib/sdk/runtime/http-client.js +81 -0
- package/generators/lib/sdk/runtime/resource-client.js +59 -0
- package/generators/lib/sdk/runtime/storage.js +31 -0
- package/generators/lib/templates/dashboard-catalog.js +1 -1
- package/generators/lib/templates/db-connection-env.js +1 -1
- package/generators/lib/templates/dbschema-catalog.js +1 -1
- package/generators/lib/templates/field-validation-catalog.js +1 -1
- package/generators/lib/templates/mysql-template.js +1 -1
- package/generators/lib/templates/oracle-template.js +1 -1
- package/generators/lib/templates/postgres-template.js +1 -1
- package/generators/lib/templates/query-declarative-catalog.js +1 -1
- package/generators/lib/templates/sqlite-template.js +1 -1
- package/generators/lib/utils/cli-output.js +40 -0
- package/generators/lib/utils/config-resolver.js +61 -0
- package/generators/lib/utils/database-introspector.js +28 -5
- package/integrity-manifest.json +18 -18
- package/package.json +1 -1
- package/scripts/verify-integrity.js +1 -1
- package/server.js +1 -1
- package/src/components/handlers/adjust_handler.js +1 -1
- package/src/components/handlers/audit_handler.js +1 -1
- package/src/components/handlers/delete_handler.js +1 -1
- package/src/components/handlers/export_handler.js +1 -1
- package/src/components/handlers/import_handler.js +1 -1
- package/src/components/handlers/insert_handler.js +1 -1
- package/src/components/handlers/update_handler.js +1 -1
- package/src/components/handlers/upload_handler.js +1 -1
- package/src/components/handlers/workflow_handler.js +1 -1
- package/src/components/integrations/webhook.js +1 -1
- package/src/consumers/baseConsumer.js +1 -1
- package/src/consumers/declarativeMapper.js +1 -1
- package/src/consumers/handlers/apiHandler.js +1 -1
- package/src/consumers/handlers/consoleHandler.js +1 -1
- package/src/consumers/handlers/databaseHandler.js +1 -1
- package/src/consumers/handlers/index.js +1 -1
- package/src/consumers/handlers/kafkaHandler.js +1 -1
- package/src/consumers/index.js +1 -1
- package/src/consumers/messageTransformer.js +1 -1
- package/src/consumers/validator.js +1 -1
- package/src/core/db/dialect/base-dialect.js +1 -1
- package/src/core/db/dialect/index.js +1 -1
- package/src/core/db/dialect/mysql-dialect.js +1 -1
- package/src/core/db/dialect/oracle-dialect.js +1 -1
- package/src/core/db/dialect/postgres-dialect.js +1 -1
- package/src/core/db/dialect/sqlite-dialect.js +1 -1
- package/src/core/db/flatten-helper.js +1 -1
- package/src/core/db/query-builder-error.js +1 -1
- package/src/core/db/query-builder.js +1 -1
- package/src/core/db/relation-helper.js +1 -1
- package/src/core/handlers/delete_handler.js +1 -1
- package/src/core/handlers/insert_handler.js +1 -1
- package/src/core/handlers/update_handler.js +1 -1
- package/src/core/models/base-model.js +1 -1
- package/src/core/utils/cache-manager.js +1 -1
- package/src/core/utils/component-engine.js +1 -1
- package/src/core/utils/context-builder.js +1 -1
- package/src/core/utils/datetime-formatter.js +1 -1
- package/src/core/utils/datetime-parser.js +1 -1
- package/src/core/utils/db.js +1 -1
- package/src/core/utils/logger.js +1 -1
- package/src/core/utils/payload-loader.js +1 -1
- package/src/core/utils/security-checks.js +1 -1
- package/src/middleware/body-options.js +1 -1
- package/src/middleware/cors.js +1 -1
- package/src/middleware/idempotency.js +1 -1
- package/src/middleware/rate-limiter.js +1 -1
- package/src/middleware/request-logger.js +1 -1
- package/src/middleware/security-headers.js +1 -1
- package/src/models/base-model-mysql.js +1 -1
- package/src/models/base-model-oracle.js +1 -1
- package/src/models/base-model-sqlite.js +1 -1
- package/src/models/base-model.js +1 -1
- package/src/pro/caching/redis-client.js +1 -1
- package/src/pro/caching/redis-helper.js +1 -1
- package/src/pro/consumers/baseConsumer.js +1 -1
- package/src/pro/consumers/declarativeMapper.js +1 -1
- package/src/pro/consumers/handlers/apiHandler.js +1 -1
- package/src/pro/consumers/handlers/consoleHandler.js +1 -1
- package/src/pro/consumers/handlers/databaseHandler.js +1 -1
- package/src/pro/consumers/handlers/index.js +1 -1
- package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
- package/src/pro/consumers/index.js +1 -1
- package/src/pro/consumers/messageTransformer.js +1 -1
- package/src/pro/consumers/validator.js +1 -1
- package/src/pro/database/base-model-mysql.js +1 -1
- package/src/pro/database/base-model-oracle.js +1 -1
- package/src/pro/database/base-model-sqlite.js +1 -1
- package/src/pro/database/db-mysql.js +1 -1
- package/src/pro/database/db-oracle.js +1 -1
- package/src/pro/database/db-sqlite.js +1 -1
- package/src/pro/excel/excel-generator.js +1 -1
- package/src/pro/excel/excel-parser.js +1 -1
- package/src/pro/excel/export-service.js +1 -1
- package/src/pro/excel/export_handler.js +1 -1
- package/src/pro/excel/import-service.js +1 -1
- package/src/pro/excel/import-validator.js +1 -1
- package/src/pro/excel/import_handler.js +1 -1
- package/src/pro/excel/upsert-builder.js +1 -1
- package/src/pro/idgen/idgen-routes.js +1 -1
- package/src/pro/integrations/lookup-resolver.js +1 -1
- package/src/pro/integrations/upload-handler-v2.js +1 -1
- package/src/pro/integrations/upload-handler.js +1 -1
- package/src/pro/integrations/webhook.js +1 -1
- package/src/pro/locking/lock-routes.js +1 -1
- package/src/pro/locking/resource-lock-manager.js +1 -1
- package/src/pro/messaging/kafkaConsumerService.js +1 -1
- package/src/pro/messaging/kafkaService.js +1 -1
- package/src/pro/messaging/messagehubService.js +1 -1
- package/src/pro/messaging/rabbitmqService.js +1 -1
- package/src/pro/scheduler/job-manager.js +1 -1
- package/src/pro/scheduler/job-routes.js +1 -1
- package/src/pro/scheduler/job-validator.js +1 -1
- package/src/pro/storage/base-storage-provider.js +1 -1
- package/src/pro/storage/file-metadata-helper.js +1 -1
- package/src/pro/storage/index.js +1 -1
- package/src/pro/storage/local-storage-provider.js +1 -1
- package/src/pro/storage/s3-storage-provider.js +1 -1
- package/src/pro/storage/upload-cleanup-job.js +1 -1
- package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
- package/src/pro/storage/upload-pending-tracker.js +1 -1
- package/src/pro/websocket/broadcast-helper.js +1 -1
- package/src/pro/websocket/index.js +1 -1
- package/src/pro/websocket/livesync-server.js +1 -1
- package/src/pro/websocket/ws-broadcaster.js +1 -1
- package/src/services/export-service.js +1 -1
- package/src/services/import-service.js +1 -1
- package/src/services/kafkaConsumerService.js +1 -1
- package/src/services/kafkaService.js +1 -1
- package/src/services/messagehubService.js +1 -1
- package/src/services/rabbitmqService.js +1 -1
- package/src/utils/cache-invalidation-registry.js +1 -1
- package/src/utils/cache-manager.js +1 -1
- package/src/utils/component-engine.js +1 -1
- package/src/utils/config-extractor.js +1 -1
- package/src/utils/consumerLogger.js +1 -1
- package/src/utils/context-builder.js +1 -1
- package/src/utils/dashboard-helpers.js +1 -1
- package/src/utils/dateHelper.js +1 -1
- package/src/utils/datetime-formatter.js +1 -1
- package/src/utils/datetime-parser.js +1 -1
- package/src/utils/db-bootstrap.js +1 -1
- package/src/utils/db-mysql.js +1 -1
- package/src/utils/db-oracle.js +1 -1
- package/src/utils/db-sqlite.js +1 -1
- package/src/utils/db.js +1 -1
- package/src/utils/demo-generator.js +1 -1
- package/src/utils/excel-generator.js +1 -1
- package/src/utils/excel-parser.js +1 -1
- package/src/utils/file-watcher.js +1 -1
- package/src/utils/id-generator.js +1 -1
- package/src/utils/idempotency-manager.js +1 -1
- package/src/utils/import-validator.js +1 -1
- package/src/utils/license-client.js +1 -1
- package/src/utils/lock-manager.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/lookup-resolver.js +1 -1
- package/src/utils/payload-loader.js +1 -1
- package/src/utils/processor-response.js +1 -1
- package/src/utils/rabbitmq.js +1 -1
- package/src/utils/redis-client.js +1 -1
- package/src/utils/redis-helper.js +1 -1
- package/src/utils/request-scope.js +1 -1
- package/src/utils/security-checks.js +1 -1
- package/src/utils/service-resolver.js +1 -1
- package/src/utils/shutdown-coordinator.js +1 -1
- package/src/utils/soft-delete-dashboard-guard.js +1 -1
- package/src/utils/sql-table-extractor.js +1 -1
- package/src/utils/trusted-keys.js +1 -1
- package/src/utils/upload-handler.js +1 -1
- package/src/utils/upsert-builder.js +1 -1
- package/src/utils/workflow-hook-executor.js +1 -1
|
@@ -34,6 +34,8 @@ const DemoGenerator = require('../../src/utils/demo-generator');
|
|
|
34
34
|
const projectRegistry = require('../../lib/utils/project-registry');
|
|
35
35
|
const cliOutput = require('../../lib/utils/cli-output');
|
|
36
36
|
const endpointSchemaValidator = require('../../lib/payload/endpoint-schema-validator');
|
|
37
|
+
const { deriveFieldProjections, augmentProjectionsForSoftDelete } = require('../../lib/payload/field-projections');
|
|
38
|
+
const configResolver = require('../../lib/utils/config-resolver');
|
|
37
39
|
|
|
38
40
|
function hasAuditRequired(payload) {
|
|
39
41
|
if (!payload || !payload.fieldPolicy) return false;
|
|
@@ -143,14 +145,12 @@ module.exports = {
|
|
|
143
145
|
async handler(args) {
|
|
144
146
|
const startTime = Date.now();
|
|
145
147
|
let muted = false;
|
|
148
|
+
let summary = null;
|
|
146
149
|
|
|
147
150
|
try {
|
|
148
151
|
const project = ArgumentValidator.validateProjectName(args.project);
|
|
149
152
|
const endpoint = ArgumentValidator.validateEndpointName(args.name);
|
|
150
153
|
const payloadFile = ArgumentValidator.validatePayloadName(args.payload);
|
|
151
|
-
const database = args.database
|
|
152
|
-
? ArgumentValidator.validateDatabaseType(args.database)
|
|
153
|
-
: 'postgres';
|
|
154
154
|
const force = !!args.force;
|
|
155
155
|
const createExamples = !!args['create-examples'];
|
|
156
156
|
const skipSqlValidation = !!args['skip-sql-validation'];
|
|
@@ -161,6 +161,30 @@ module.exports = {
|
|
|
161
161
|
? args.config.trim()
|
|
162
162
|
: null;
|
|
163
163
|
|
|
164
|
+
// Resolusi tipe database:
|
|
165
|
+
// 1. --database eksplisit → dipakai apa adanya (prioritas tertinggi)
|
|
166
|
+
// 2. Auto-deteksi DB_TYPE dari config aktif (--config, atau default
|
|
167
|
+
// config .restforge/defaults.json) → mengikuti DB_TYPE project
|
|
168
|
+
// 3. Fallback 'postgres' bila tidak ada config yang bisa di-resolve
|
|
169
|
+
let database;
|
|
170
|
+
let databaseSource;
|
|
171
|
+
if (args.database) {
|
|
172
|
+
database = ArgumentValidator.validateDatabaseType(args.database);
|
|
173
|
+
databaseSource = 'flag';
|
|
174
|
+
} else {
|
|
175
|
+
const resolvedCfg = configResolver.resolveConfig(configArg, process.cwd());
|
|
176
|
+
const detected = resolvedCfg
|
|
177
|
+
? configResolver.readDatabaseTypeFromConfig(resolvedCfg.path)
|
|
178
|
+
: null;
|
|
179
|
+
if (detected) {
|
|
180
|
+
database = detected;
|
|
181
|
+
databaseSource = resolvedCfg.source === 'default' ? 'config-default' : 'config';
|
|
182
|
+
} else {
|
|
183
|
+
database = 'postgres';
|
|
184
|
+
databaseSource = 'fallback';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
164
188
|
if (!verbose) {
|
|
165
189
|
cliOutput.mute();
|
|
166
190
|
muted = true;
|
|
@@ -170,12 +194,17 @@ module.exports = {
|
|
|
170
194
|
console.log(` Project: ${project}`);
|
|
171
195
|
console.log(` Endpoint: ${endpoint}`);
|
|
172
196
|
console.log(` Payload: ${payloadFile}`);
|
|
173
|
-
|
|
197
|
+
const databaseNote = databaseSource === 'config'
|
|
198
|
+
? ' (auto-detected from --config)'
|
|
199
|
+
: databaseSource === 'config-default'
|
|
200
|
+
? ' (auto-detected from default config)'
|
|
201
|
+
: '';
|
|
202
|
+
console.log(` Database: ${database}${databaseNote}`);
|
|
174
203
|
console.log(` Force overwrite: ${force}`);
|
|
175
204
|
console.log('');
|
|
176
205
|
|
|
177
206
|
const cwd = process.cwd();
|
|
178
|
-
|
|
207
|
+
summary = {
|
|
179
208
|
config: { project, endpoint, database, force },
|
|
180
209
|
payload: null,
|
|
181
210
|
archive: null,
|
|
@@ -204,7 +233,7 @@ module.exports = {
|
|
|
204
233
|
const schemaResult = await endpointSchemaValidator.validateEndpointSchema({
|
|
205
234
|
payload,
|
|
206
235
|
payloadFileName: path.basename(payloadFile),
|
|
207
|
-
payloadFilePath:
|
|
236
|
+
payloadFilePath: rawPayload._payloadPath,
|
|
208
237
|
configArg,
|
|
209
238
|
skipSchemaCheck,
|
|
210
239
|
workingDir: cwd
|
|
@@ -216,6 +245,37 @@ module.exports = {
|
|
|
216
245
|
summary.schemaValidation = schemaResult;
|
|
217
246
|
summary.config.config = configArg || null;
|
|
218
247
|
|
|
248
|
+
// Derivasi field projections dan embed ke payload sebagai _fieldProjections.
|
|
249
|
+
// Template membaca ini untuk emit schemaFields/readableFields/datatablesFields.
|
|
250
|
+
// Fallback ke fieldName bila DB tidak tersedia (skipSchemaCheck atau projectionInputs absent).
|
|
251
|
+
{
|
|
252
|
+
const fn = payload.fieldName || [];
|
|
253
|
+
let fieldProjections;
|
|
254
|
+
if (schemaResult.status === 'ok' && schemaResult.projectionInputs) {
|
|
255
|
+
fieldProjections = deriveFieldProjections({
|
|
256
|
+
fieldName: fn,
|
|
257
|
+
physicalColumns: schemaResult.projectionInputs.physicalColumns,
|
|
258
|
+
readSourceColumns: schemaResult.projectionInputs.readSourceColumns,
|
|
259
|
+
datatablesColumns: schemaResult.projectionInputs.datatablesColumns,
|
|
260
|
+
overrides: {
|
|
261
|
+
readableFields: payload.readableFields,
|
|
262
|
+
datatablesFields: payload.datatablesFields
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
// skipSchemaCheck atau projectionInputs tidak tersedia:
|
|
267
|
+
// fallback langsung ke fieldName untuk semua proyeksi tanpa DB.
|
|
268
|
+
// Override payload.readableFields / datatablesFields diterapkan bila ada.
|
|
269
|
+
fieldProjections = {
|
|
270
|
+
schemaFields: fn.slice(),
|
|
271
|
+
readableFields: payload.readableFields || fn.slice(),
|
|
272
|
+
datatablesFields: payload.datatablesFields || fn.slice()
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
augmentProjectionsForSoftDelete(fieldProjections, payload);
|
|
276
|
+
payload._fieldProjections = fieldProjections;
|
|
277
|
+
}
|
|
278
|
+
|
|
219
279
|
const registry = projectRegistry.loadProjectRegistry();
|
|
220
280
|
if (registry.projects[project]) {
|
|
221
281
|
const existing = registry.projects[project];
|
|
@@ -312,6 +372,9 @@ module.exports = {
|
|
|
312
372
|
// cli-entry.js men-print `Error: <message>` ke stderr saat handler
|
|
313
373
|
// re-throw (lihat handler dispatch). Jangan double-print di sini.
|
|
314
374
|
if (muted) cliOutput.unmute();
|
|
375
|
+
if (summary && summary.config) {
|
|
376
|
+
cliOutput.printCreatePartial(summary);
|
|
377
|
+
}
|
|
315
378
|
throw error;
|
|
316
379
|
}
|
|
317
380
|
}
|
|
@@ -32,10 +32,11 @@ module.exports = {
|
|
|
32
32
|
description: 'Sync only a specific table (default: all)'
|
|
33
33
|
},
|
|
34
34
|
'expand-fk': {
|
|
35
|
-
type: '
|
|
35
|
+
type: 'string',
|
|
36
36
|
required: false,
|
|
37
|
-
default:
|
|
38
|
-
|
|
37
|
+
default: null,
|
|
38
|
+
bareDefault: 'both',
|
|
39
|
+
description: 'Generate JOIN configuration from foreign keys: creates SQL file query/<table>-join.sql. Values: "both" (updates datatablesQuery and viewQuery) or "datatables-only" (updates datatablesQuery only, viewQuery unchanged). Bare --expand-fk (without value) defaults to "both". Requires --table. If --fk-columns is empty, display columns per FK are auto-selected (name → code → primary key)'
|
|
39
40
|
},
|
|
40
41
|
'fk-columns': {
|
|
41
42
|
type: 'string',
|
|
@@ -47,16 +48,25 @@ module.exports = {
|
|
|
47
48
|
examples: [
|
|
48
49
|
'npx restforge payload sync --config=db.env',
|
|
49
50
|
'npx restforge payload sync --config=db.env --table=users',
|
|
50
|
-
'npx restforge payload sync --table=visitors --expand-fk',
|
|
51
|
-
'npx restforge payload sync --table=visitors --expand-fk
|
|
51
|
+
'npx restforge payload sync --table=visitors --expand-fk=both',
|
|
52
|
+
'npx restforge payload sync --table=visitors --expand-fk=datatables-only',
|
|
53
|
+
'npx restforge payload sync --table=visitors --expand-fk=both --fk-columns=visitor_categories.category_code,visitor_categories.category_name'
|
|
52
54
|
],
|
|
53
55
|
async handler(args) {
|
|
56
|
+
const expandFkRaw = args['expand-fk'];
|
|
57
|
+
let expandFkMode = null;
|
|
58
|
+
if (expandFkRaw === 'both' || expandFkRaw === 'datatables-only') {
|
|
59
|
+
expandFkMode = expandFkRaw;
|
|
60
|
+
} else if (expandFkRaw !== null && expandFkRaw !== undefined) {
|
|
61
|
+
throw new Error(`Invalid --expand-fk value "${expandFkRaw}". Valid values: both, datatables-only`);
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
const generator = new PayloadGenerator();
|
|
55
65
|
await generator.run({
|
|
56
66
|
config: args.config,
|
|
57
67
|
table: args.table || null,
|
|
58
68
|
sync: true,
|
|
59
|
-
|
|
69
|
+
expandFkMode,
|
|
60
70
|
fkColumns: args['fk-columns'] || null
|
|
61
71
|
});
|
|
62
72
|
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* RESTForge yang sudah ada. Setelah validasi prasyarat (phase 00), handler
|
|
8
8
|
* menulis dua file SDF auth ber-prefix `rfx` ke `--schema-path` (Fungsi 1),
|
|
9
9
|
* lalu membuat tabelnya di DB via primitif dbschema-kit langsung (Fungsi 2),
|
|
10
|
-
* lalu menulis component middleware + router auth
|
|
11
|
-
* lalu menulis
|
|
10
|
+
* lalu menulis component middleware + router auth termasuk route google (Fungsi 3a),
|
|
11
|
+
* lalu menulis ketujuh processor auth termasuk google (Fungsi 3b), lalu
|
|
12
12
|
* menginjeksi variabel env auth ke `--config` dan memverifikasi/mencatat
|
|
13
13
|
* dependency runtime `bcrypt`+`jsonwebtoken` (Fungsi tambahan, phase 05),
|
|
14
14
|
* lalu mencetak ringkasan akhir.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Contract: project sdk
|
|
5
|
+
*
|
|
6
|
+
* Menghasilkan SDK JavaScript untuk satu project RESTForge — satu layer tipis di
|
|
7
|
+
* atas REST API hasil generate, dipakai frontend apa pun tanpa menulis ulang
|
|
8
|
+
* boilerplate fetch/$.ajax.
|
|
9
|
+
*
|
|
10
|
+
* Rujukan desain: docs/plan/sdk-generator-command.md.
|
|
11
|
+
*
|
|
12
|
+
* Versi awal:
|
|
13
|
+
* - TANPA auth (flag --with-auth & core/auth-client.js menyusul terpisah).
|
|
14
|
+
* - --generate hanya MENULIS source buildable (src/, package.json, tsup.config.js);
|
|
15
|
+
* `npm install && npm run build` adalah langkah terpisah milik user.
|
|
16
|
+
* - Sumber kebenaran resource = metadata/<project>.json (key endpoint = slug = segment route).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { validateSafeName } = require('../../lib/utils/path-validator');
|
|
20
|
+
const { generateSdk, resolveBaseUrl } = require('../../lib/sdk/generator');
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
resource: 'project',
|
|
24
|
+
verb: 'sdk',
|
|
25
|
+
description: 'Generate a JavaScript SDK for a project (derived from backend metadata + payload)',
|
|
26
|
+
category: 'generation',
|
|
27
|
+
flags: {
|
|
28
|
+
project: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
required: true,
|
|
31
|
+
description: 'Target project name (also the SDK package name)'
|
|
32
|
+
},
|
|
33
|
+
generate: {
|
|
34
|
+
type: 'boolean',
|
|
35
|
+
required: true,
|
|
36
|
+
description: 'Trigger SDK source generation'
|
|
37
|
+
},
|
|
38
|
+
'sdk-path': {
|
|
39
|
+
type: 'string',
|
|
40
|
+
required: false,
|
|
41
|
+
default: null,
|
|
42
|
+
description: 'Output folder for the SDK source (default: <project-root>/sdk)'
|
|
43
|
+
},
|
|
44
|
+
'base-url': {
|
|
45
|
+
type: 'string',
|
|
46
|
+
required: false,
|
|
47
|
+
default: null,
|
|
48
|
+
description: 'Override the API base URL baked into sdk-client.js (default: derived from the project config)'
|
|
49
|
+
},
|
|
50
|
+
force: {
|
|
51
|
+
type: 'boolean',
|
|
52
|
+
required: false,
|
|
53
|
+
default: false,
|
|
54
|
+
description: 'Overwrite existing SDK source'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
examples: [
|
|
58
|
+
'npx restforge project sdk --generate --project=myapp',
|
|
59
|
+
'npx restforge project sdk --generate --project=myapp --sdk-path=./client-sdk',
|
|
60
|
+
'npx restforge project sdk --generate --project=myapp --force'
|
|
61
|
+
],
|
|
62
|
+
async handler(args) {
|
|
63
|
+
const project = validateSafeName(args.project, 'project');
|
|
64
|
+
|
|
65
|
+
if (args.generate !== true) {
|
|
66
|
+
const err = new Error('The --generate flag must be set to run SDK generation.');
|
|
67
|
+
err.exitCode = 2;
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const workingDir = process.cwd();
|
|
72
|
+
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log(`Generating SDK for project '${project}'...`);
|
|
75
|
+
console.log('');
|
|
76
|
+
|
|
77
|
+
const baseUrl = resolveBaseUrl({
|
|
78
|
+
workingDir,
|
|
79
|
+
project,
|
|
80
|
+
override: args['base-url'],
|
|
81
|
+
log: (line) => console.log(` ${line}`)
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const result = generateSdk({
|
|
85
|
+
workingDir,
|
|
86
|
+
project,
|
|
87
|
+
sdkPath: args['sdk-path'],
|
|
88
|
+
baseUrl,
|
|
89
|
+
force: args.force === true,
|
|
90
|
+
log: (line) => console.log(` ${line}`)
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log('==========================================');
|
|
95
|
+
console.log('SDK GENERATION COMPLETE');
|
|
96
|
+
console.log('==========================================');
|
|
97
|
+
console.log(`Project : ${project}`);
|
|
98
|
+
console.log(`Output : ${result.outputDir}`);
|
|
99
|
+
console.log(`Base URL : ${result.baseUrl}`);
|
|
100
|
+
console.log(`Auth : ${result.hasAuth ? 'enabled (client.auth)' : 'none'}`);
|
|
101
|
+
console.log(`Resources : ${result.resources.length} (${result.resources.join(', ')})`);
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('Next steps (optional, owned by you):');
|
|
104
|
+
console.log(` cd ${result.outputDir}`);
|
|
105
|
+
console.log(' npm install');
|
|
106
|
+
console.log(' npm run build');
|
|
107
|
+
console.log(' npm run deploy # copy into a frontend app js/ folder (asks for target)');
|
|
108
|
+
console.log(' # or: node deploy.mjs <target-app-js-folder>');
|
|
109
|
+
console.log('==========================================');
|
|
110
|
+
console.log('');
|
|
111
|
+
}
|
|
112
|
+
};
|
|
@@ -119,6 +119,12 @@ function parseArgs(argv, contract) {
|
|
|
119
119
|
|
|
120
120
|
if (!valueProvided) {
|
|
121
121
|
if (i + 1 >= argv.length || argv[i + 1].startsWith('--')) {
|
|
122
|
+
if (flagDef.bareDefault !== undefined) {
|
|
123
|
+
args[flagName] = flagDef.bareDefault;
|
|
124
|
+
seenFlags.add(flagName);
|
|
125
|
+
i += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
122
128
|
errors.push(`Flag --${flagName} requires a value`);
|
|
123
129
|
i += 1;
|
|
124
130
|
continue;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Generator processor auth (Fungsi 3b). Merender
|
|
4
|
+
* Generator processor auth (Fungsi 3b). Merender aset template di
|
|
5
5
|
* `templates/processor/` via template-renderer (Phase 03), lalu menulis ke
|
|
6
6
|
* lokasi target memakai writeFileWithBackup — perilaku force/skip identik
|
|
7
7
|
* Fungsi 1/3a: tanpa --force file existing di-skip, dengan --force
|
|
8
8
|
* di-overwrite + backup.
|
|
9
9
|
*
|
|
10
|
-
* `google.js`
|
|
10
|
+
* `google.js` (Sign in with Google) disertakan; endpoint membutuhkan
|
|
11
|
+
* GOOGLE_CLIENT_ID di env (di-inject env-injector) dan ID token dari Google
|
|
12
|
+
* Identity Services di sisi frontend.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
const fs = require('fs');
|
|
@@ -19,7 +21,7 @@ const { AUTH_USER_TABLE, AUTH_REFRESH_TOKEN_TABLE, AUTH_MIDDLEWARE_NAME } = requ
|
|
|
19
21
|
|
|
20
22
|
const TEMPLATES_DIR = path.join(__dirname, 'templates', 'processor');
|
|
21
23
|
|
|
22
|
-
const PROCESSOR_NAMES = ['register', 'login', 'refresh', 'logout', 'me', 'reset-password'];
|
|
24
|
+
const PROCESSOR_NAMES = ['register', 'login', 'google', 'refresh', 'logout', 'me', 'reset-password'];
|
|
23
25
|
|
|
24
26
|
const PARAMS = {
|
|
25
27
|
AUTH_USER_TABLE,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Processor: google (Sign in with Google)
|
|
5
|
+
* Verifikasi Google ID token, lalu find-or-create user (identitas = email) dan
|
|
6
|
+
* terbitkan JWT access token + refresh token (sama seperti login biasa).
|
|
7
|
+
* Method: POST body: { credential: "<google-id-token>" }
|
|
8
|
+
*
|
|
9
|
+
* Verifikasi ID token memakai endpoint tokeninfo Google (tanpa dependensi npm
|
|
10
|
+
* tambahan). Untuk produksi, pertimbangkan google-auth-library (verifikasi JWKS
|
|
11
|
+
* lokal) agar tidak bergantung pada round-trip ke Google tiap request.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const bcrypt = require('bcrypt');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const {
|
|
17
|
+
generateAccessToken,
|
|
18
|
+
getAccessTokenExpiryMin
|
|
19
|
+
} = require('../../../../components/handlers/{{AUTH_MIDDLEWARE_NAME}}');
|
|
20
|
+
|
|
21
|
+
const BCRYPT_ROUNDS = 12;
|
|
22
|
+
|
|
23
|
+
function getRefreshExpiryDays() {
|
|
24
|
+
return parseInt(process.env.REFRESH_TOKEN_EXPIRY_DAYS || '30', 10);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function verifyGoogleIdToken(credential) {
|
|
28
|
+
const url = 'https://oauth2.googleapis.com/tokeninfo?id_token=' + encodeURIComponent(credential);
|
|
29
|
+
const res = await fetch(url);
|
|
30
|
+
if (!res.ok) return null;
|
|
31
|
+
return res.json();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
async process(input, services, req) {
|
|
36
|
+
const { db, logger } = services;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const { credential } = input;
|
|
40
|
+
|
|
41
|
+
if (!credential) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
statusCode: 400,
|
|
45
|
+
message: 'Field credential (Google ID token) is required.',
|
|
46
|
+
timestamp: new Date().toISOString()
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const clientId = process.env.GOOGLE_CLIENT_ID;
|
|
51
|
+
if (!clientId) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
statusCode: 501,
|
|
55
|
+
message: 'Google login is not configured on the server (GOOGLE_CLIENT_ID is empty).',
|
|
56
|
+
timestamp: new Date().toISOString()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const info = await verifyGoogleIdToken(credential);
|
|
61
|
+
if (!info || !info.email) {
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
statusCode: 401,
|
|
65
|
+
message: 'Invalid Google token.',
|
|
66
|
+
timestamp: new Date().toISOString()
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Pastikan token memang ditujukan untuk aplikasi ini.
|
|
71
|
+
if (info.aud !== clientId) {
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
statusCode: 401,
|
|
75
|
+
message: 'Google token was not issued for this application.',
|
|
76
|
+
timestamp: new Date().toISOString()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Google menandai email_verified sebagai string 'true'.
|
|
81
|
+
if (info.email_verified !== 'true' && info.email_verified !== true) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
statusCode: 403,
|
|
85
|
+
message: 'Google email is not verified.',
|
|
86
|
+
timestamp: new Date().toISOString()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const email = String(info.email).toLowerCase();
|
|
91
|
+
const fullName = info.name || null;
|
|
92
|
+
|
|
93
|
+
// Identitas = email. Satukan dengan akun manual ber-email sama.
|
|
94
|
+
const rows = await db.executeQuery(
|
|
95
|
+
`SELECT user_id, username, email, full_name, is_active, is_locked
|
|
96
|
+
FROM public.{{AUTH_USER_TABLE}} WHERE username = $1`,
|
|
97
|
+
[email]
|
|
98
|
+
);
|
|
99
|
+
let user = rows[0] || null;
|
|
100
|
+
|
|
101
|
+
if (user) {
|
|
102
|
+
if (!user.is_active || user.is_locked) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
statusCode: 403,
|
|
106
|
+
message: 'Account is inactive or locked.',
|
|
107
|
+
timestamp: new Date().toISOString()
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
await db.executeQuery(
|
|
111
|
+
`UPDATE public.{{AUTH_USER_TABLE}}
|
|
112
|
+
SET last_login_at = NOW(), failed_login_count = 0, updated_at = NOW()
|
|
113
|
+
WHERE user_id = $1`,
|
|
114
|
+
[user.user_id]
|
|
115
|
+
);
|
|
116
|
+
} else {
|
|
117
|
+
// User baru via Google: password acak (tidak dapat dipakai login manual
|
|
118
|
+
// sampai user reset password). password_hash tetap non-null sesuai schema.
|
|
119
|
+
const randomPw = crypto.randomBytes(32).toString('hex');
|
|
120
|
+
const passwordHash = await bcrypt.hash(randomPw, BCRYPT_ROUNDS);
|
|
121
|
+
const userId = crypto.randomUUID();
|
|
122
|
+
|
|
123
|
+
await db.executeQuery(
|
|
124
|
+
`INSERT INTO public.{{AUTH_USER_TABLE}}
|
|
125
|
+
(user_id, username, email, full_name, password_hash,
|
|
126
|
+
is_active, is_locked, failed_login_count,
|
|
127
|
+
last_login_at, password_changed_at, created_at)
|
|
128
|
+
VALUES ($1, $2, $3, $4, $5, TRUE, FALSE, 0, NOW(), NOW(), NOW())`,
|
|
129
|
+
[userId, email, email, fullName, passwordHash]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
user = { user_id: userId, username: email, email: email, full_name: fullName };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const accessToken = generateAccessToken(user);
|
|
136
|
+
const expiresIn = getAccessTokenExpiryMin() * 60;
|
|
137
|
+
|
|
138
|
+
const refreshTokenRaw = `${user.user_id}:${crypto.randomBytes(64).toString('hex')}`;
|
|
139
|
+
const refreshTokenHash = await bcrypt.hash(refreshTokenRaw, BCRYPT_ROUNDS);
|
|
140
|
+
const refreshExpiresAt = new Date();
|
|
141
|
+
refreshExpiresAt.setDate(refreshExpiresAt.getDate() + getRefreshExpiryDays());
|
|
142
|
+
|
|
143
|
+
await db.executeQuery(
|
|
144
|
+
`INSERT INTO public.{{AUTH_REFRESH_TOKEN_TABLE}}
|
|
145
|
+
(token_id, user_id, token_hash, expires_at, created_at)
|
|
146
|
+
VALUES ($1, $2, $3, $4, NOW())`,
|
|
147
|
+
[crypto.randomUUID(), user.user_id, refreshTokenHash, refreshExpiresAt]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
statusCode: 200,
|
|
153
|
+
message: 'Google login successful.',
|
|
154
|
+
data: {
|
|
155
|
+
access_token: accessToken,
|
|
156
|
+
refresh_token: refreshTokenRaw,
|
|
157
|
+
token_type: 'Bearer',
|
|
158
|
+
expires_in: expiresIn,
|
|
159
|
+
user: {
|
|
160
|
+
user_id: user.user_id,
|
|
161
|
+
username: user.username,
|
|
162
|
+
email: user.email,
|
|
163
|
+
full_name: user.full_name
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
timestamp: new Date().toISOString()
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.error({ error: error.message }, '[auth-google] Unexpected error');
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
statusCode: 500,
|
|
173
|
+
message: 'An internal server error occurred.',
|
|
174
|
+
timestamp: new Date().toISOString()
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
@@ -31,7 +31,7 @@ module.exports = {
|
|
|
31
31
|
return {
|
|
32
32
|
success: false,
|
|
33
33
|
statusCode: 400,
|
|
34
|
-
message: '
|
|
34
|
+
message: 'Username and password are required.',
|
|
35
35
|
timestamp: new Date().toISOString()
|
|
36
36
|
};
|
|
37
37
|
}
|
|
@@ -49,7 +49,7 @@ module.exports = {
|
|
|
49
49
|
return {
|
|
50
50
|
success: false,
|
|
51
51
|
statusCode: 401,
|
|
52
|
-
message: '
|
|
52
|
+
message: 'Invalid username or password.',
|
|
53
53
|
timestamp: new Date().toISOString()
|
|
54
54
|
};
|
|
55
55
|
}
|
|
@@ -58,7 +58,7 @@ module.exports = {
|
|
|
58
58
|
return {
|
|
59
59
|
success: false,
|
|
60
60
|
statusCode: 403,
|
|
61
|
-
message: '
|
|
61
|
+
message: 'Account is inactive. Contact administrator.',
|
|
62
62
|
timestamp: new Date().toISOString()
|
|
63
63
|
};
|
|
64
64
|
}
|
|
@@ -67,7 +67,7 @@ module.exports = {
|
|
|
67
67
|
return {
|
|
68
68
|
success: false,
|
|
69
69
|
statusCode: 403,
|
|
70
|
-
message: '
|
|
70
|
+
message: 'Account locked due to too many failed login attempts. Contact administrator.',
|
|
71
71
|
timestamp: new Date().toISOString()
|
|
72
72
|
};
|
|
73
73
|
}
|
|
@@ -86,8 +86,8 @@ module.exports = {
|
|
|
86
86
|
);
|
|
87
87
|
|
|
88
88
|
const message = shouldLock
|
|
89
|
-
? '
|
|
90
|
-
: `
|
|
89
|
+
? 'Account locked due to too many failed login attempts. Contact administrator.'
|
|
90
|
+
: `Invalid username or password. Attempts remaining: ${MAX_FAILED_LOGIN - newFailedCount}.`;
|
|
91
91
|
|
|
92
92
|
return {
|
|
93
93
|
success: false,
|
|
@@ -124,7 +124,7 @@ module.exports = {
|
|
|
124
124
|
return {
|
|
125
125
|
success: true,
|
|
126
126
|
statusCode: 200,
|
|
127
|
-
message: 'Login
|
|
127
|
+
message: 'Login successful.',
|
|
128
128
|
data: {
|
|
129
129
|
access_token: accessToken,
|
|
130
130
|
refresh_token: refreshTokenRaw,
|
|
@@ -144,7 +144,7 @@ module.exports = {
|
|
|
144
144
|
return {
|
|
145
145
|
success: false,
|
|
146
146
|
statusCode: 500,
|
|
147
|
-
message: '
|
|
147
|
+
message: 'An internal server error occurred.',
|
|
148
148
|
timestamp: new Date().toISOString()
|
|
149
149
|
};
|
|
150
150
|
}
|
|
@@ -42,7 +42,7 @@ module.exports = {
|
|
|
42
42
|
return {
|
|
43
43
|
success: true,
|
|
44
44
|
statusCode: 200,
|
|
45
|
-
message: 'Logout
|
|
45
|
+
message: 'Logout successful.',
|
|
46
46
|
timestamp: new Date().toISOString()
|
|
47
47
|
};
|
|
48
48
|
} catch (error) {
|
|
@@ -50,7 +50,7 @@ module.exports = {
|
|
|
50
50
|
return {
|
|
51
51
|
success: false,
|
|
52
52
|
statusCode: 500,
|
|
53
|
-
message: '
|
|
53
|
+
message: 'An internal server error occurred.',
|
|
54
54
|
timestamp: new Date().toISOString()
|
|
55
55
|
};
|
|
56
56
|
}
|
|
@@ -39,7 +39,7 @@ module.exports = {
|
|
|
39
39
|
return {
|
|
40
40
|
success: false,
|
|
41
41
|
statusCode: 404,
|
|
42
|
-
message: 'User
|
|
42
|
+
message: 'User not found.',
|
|
43
43
|
timestamp: new Date().toISOString()
|
|
44
44
|
};
|
|
45
45
|
}
|
|
@@ -56,7 +56,7 @@ module.exports = {
|
|
|
56
56
|
return {
|
|
57
57
|
success: false,
|
|
58
58
|
statusCode: 500,
|
|
59
|
-
message: '
|
|
59
|
+
message: 'An internal server error occurred.',
|
|
60
60
|
timestamp: new Date().toISOString()
|
|
61
61
|
};
|
|
62
62
|
}
|
|
@@ -31,7 +31,7 @@ module.exports = {
|
|
|
31
31
|
return {
|
|
32
32
|
success: false,
|
|
33
33
|
statusCode: 400,
|
|
34
|
-
message: '
|
|
34
|
+
message: 'refresh_token is required.',
|
|
35
35
|
timestamp: new Date().toISOString()
|
|
36
36
|
};
|
|
37
37
|
}
|
|
@@ -41,7 +41,7 @@ module.exports = {
|
|
|
41
41
|
return {
|
|
42
42
|
success: false,
|
|
43
43
|
statusCode: 401,
|
|
44
|
-
message: '
|
|
44
|
+
message: 'Invalid refresh token.',
|
|
45
45
|
timestamp: new Date().toISOString()
|
|
46
46
|
};
|
|
47
47
|
}
|
|
@@ -66,7 +66,7 @@ module.exports = {
|
|
|
66
66
|
return {
|
|
67
67
|
success: false,
|
|
68
68
|
statusCode: 401,
|
|
69
|
-
message: '
|
|
69
|
+
message: 'Invalid or expired refresh token.',
|
|
70
70
|
timestamp: new Date().toISOString()
|
|
71
71
|
};
|
|
72
72
|
}
|
|
@@ -83,7 +83,7 @@ module.exports = {
|
|
|
83
83
|
return {
|
|
84
84
|
success: false,
|
|
85
85
|
statusCode: 403,
|
|
86
|
-
message: 'User
|
|
86
|
+
message: 'User account is inactive or locked.',
|
|
87
87
|
timestamp: new Date().toISOString()
|
|
88
88
|
};
|
|
89
89
|
}
|
|
@@ -112,7 +112,7 @@ module.exports = {
|
|
|
112
112
|
return {
|
|
113
113
|
success: true,
|
|
114
114
|
statusCode: 200,
|
|
115
|
-
message: 'Token
|
|
115
|
+
message: 'Token refreshed successfully.',
|
|
116
116
|
data: {
|
|
117
117
|
access_token: accessToken,
|
|
118
118
|
refresh_token: newRefreshRaw,
|
|
@@ -126,7 +126,7 @@ module.exports = {
|
|
|
126
126
|
return {
|
|
127
127
|
success: false,
|
|
128
128
|
statusCode: 500,
|
|
129
|
-
message: '
|
|
129
|
+
message: 'An internal server error occurred.',
|
|
130
130
|
timestamp: new Date().toISOString()
|
|
131
131
|
};
|
|
132
132
|
}
|