@restforgejs/platform 5.2.13 → 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.
Files changed (212) hide show
  1. package/bin/drift-check-linux +0 -0
  2. package/bin/sdf-tools-linux +0 -0
  3. package/bin/sdf-tools.exe +0 -0
  4. package/build-info.json +2 -2
  5. package/cli/consumer-deploy.js +1 -1
  6. package/cli/consumer.js +1 -1
  7. package/generators/cli/endpoint/create.js +69 -6
  8. package/generators/cli/fast-track.js +4 -2
  9. package/generators/cli/payload/sync.js +16 -6
  10. package/generators/cli/project/auth.js +209 -0
  11. package/generators/cli/project/sdk.js +112 -0
  12. package/generators/lib/arg-parser.js +6 -0
  13. package/generators/lib/auth/component-generator.js +58 -0
  14. package/generators/lib/auth/dependency-checker.js +102 -0
  15. package/generators/lib/auth/env-injector.js +81 -0
  16. package/generators/lib/auth/migrate-runner.js +111 -0
  17. package/generators/lib/auth/prefix.js +22 -0
  18. package/generators/lib/auth/processor-generator.js +57 -0
  19. package/generators/lib/auth/sdf-generator.js +102 -0
  20. package/generators/lib/auth/template-renderer.js +29 -0
  21. package/generators/lib/auth/templates/processor/google.js.tmpl +178 -0
  22. package/generators/lib/auth/templates/processor/login.js.tmpl +152 -0
  23. package/generators/lib/auth/templates/processor/logout.js.tmpl +58 -0
  24. package/generators/lib/auth/templates/processor/me.js.tmpl +64 -0
  25. package/generators/lib/auth/templates/processor/refresh.js.tmpl +134 -0
  26. package/generators/lib/auth/templates/processor/register.js.tmpl +77 -0
  27. package/generators/lib/auth/templates/processor/reset-password.js.tmpl +106 -0
  28. package/generators/lib/auth/templates/rfx_auth-middleware.js.tmpl +79 -0
  29. package/generators/lib/auth/templates/rfx_auth.js.tmpl +107 -0
  30. package/generators/lib/dbschema-kit/schema-printer.js +10 -1
  31. package/generators/lib/generators/model-generator.js +46 -59
  32. package/generators/lib/help-generator.js +41 -3
  33. package/generators/lib/payload/endpoint-schema-validator.js +8 -3
  34. package/generators/lib/payload/field-projections.js +116 -0
  35. package/generators/lib/payload/payload-runner.js +164 -48
  36. package/generators/lib/payload/schema-diff.js +108 -0
  37. package/generators/lib/sdk/generator.js +719 -0
  38. package/generators/lib/sdk/naming.js +48 -0
  39. package/generators/lib/sdk/runtime/README.md.tmpl +207 -0
  40. package/generators/lib/sdk/runtime/auth-client.js +186 -0
  41. package/generators/lib/sdk/runtime/deploy.mjs.tmpl +85 -0
  42. package/generators/lib/sdk/runtime/http-client.js +81 -0
  43. package/generators/lib/sdk/runtime/resource-client.js +59 -0
  44. package/generators/lib/sdk/runtime/storage.js +31 -0
  45. package/generators/lib/templates/dashboard-catalog.js +1 -1
  46. package/generators/lib/templates/db-connection-env.js +1 -1
  47. package/generators/lib/templates/dbschema-catalog.js +1 -1
  48. package/generators/lib/templates/field-validation-catalog.js +1 -1
  49. package/generators/lib/templates/mysql-template.js +1 -1
  50. package/generators/lib/templates/oracle-template.js +1 -1
  51. package/generators/lib/templates/postgres-template.js +1 -1
  52. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  53. package/generators/lib/templates/sqlite-template.js +1 -1
  54. package/generators/lib/utils/cli-output.js +40 -0
  55. package/generators/lib/utils/config-resolver.js +61 -0
  56. package/generators/lib/utils/database-introspector.js +28 -5
  57. package/integrity-manifest.json +18 -18
  58. package/package.json +1 -1
  59. package/scripts/verify-integrity.js +1 -1
  60. package/server.js +1 -1
  61. package/src/components/handlers/adjust_handler.js +1 -1
  62. package/src/components/handlers/audit_handler.js +1 -1
  63. package/src/components/handlers/delete_handler.js +1 -1
  64. package/src/components/handlers/export_handler.js +1 -1
  65. package/src/components/handlers/import_handler.js +1 -1
  66. package/src/components/handlers/insert_handler.js +1 -1
  67. package/src/components/handlers/update_handler.js +1 -1
  68. package/src/components/handlers/upload_handler.js +1 -1
  69. package/src/components/handlers/workflow_handler.js +1 -1
  70. package/src/components/integrations/webhook.js +1 -1
  71. package/src/consumers/baseConsumer.js +1 -1
  72. package/src/consumers/declarativeMapper.js +1 -1
  73. package/src/consumers/handlers/apiHandler.js +1 -1
  74. package/src/consumers/handlers/consoleHandler.js +1 -1
  75. package/src/consumers/handlers/databaseHandler.js +1 -1
  76. package/src/consumers/handlers/index.js +1 -1
  77. package/src/consumers/handlers/kafkaHandler.js +1 -1
  78. package/src/consumers/index.js +1 -1
  79. package/src/consumers/messageTransformer.js +1 -1
  80. package/src/consumers/validator.js +1 -1
  81. package/src/core/db/dialect/base-dialect.js +1 -1
  82. package/src/core/db/dialect/index.js +1 -1
  83. package/src/core/db/dialect/mysql-dialect.js +1 -1
  84. package/src/core/db/dialect/oracle-dialect.js +1 -1
  85. package/src/core/db/dialect/postgres-dialect.js +1 -1
  86. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  87. package/src/core/db/flatten-helper.js +1 -1
  88. package/src/core/db/query-builder-error.js +1 -1
  89. package/src/core/db/query-builder.js +1 -1
  90. package/src/core/db/relation-helper.js +1 -1
  91. package/src/core/handlers/delete_handler.js +1 -1
  92. package/src/core/handlers/insert_handler.js +1 -1
  93. package/src/core/handlers/update_handler.js +1 -1
  94. package/src/core/models/base-model.js +1 -1
  95. package/src/core/utils/cache-manager.js +1 -1
  96. package/src/core/utils/component-engine.js +1 -1
  97. package/src/core/utils/context-builder.js +1 -1
  98. package/src/core/utils/datetime-formatter.js +1 -1
  99. package/src/core/utils/datetime-parser.js +1 -1
  100. package/src/core/utils/db.js +1 -1
  101. package/src/core/utils/logger.js +1 -1
  102. package/src/core/utils/payload-loader.js +1 -1
  103. package/src/core/utils/security-checks.js +1 -1
  104. package/src/middleware/body-options.js +1 -1
  105. package/src/middleware/cors.js +1 -1
  106. package/src/middleware/idempotency.js +1 -1
  107. package/src/middleware/rate-limiter.js +1 -1
  108. package/src/middleware/request-logger.js +1 -1
  109. package/src/middleware/security-headers.js +1 -1
  110. package/src/models/base-model-mysql.js +1 -1
  111. package/src/models/base-model-oracle.js +1 -1
  112. package/src/models/base-model-sqlite.js +1 -1
  113. package/src/models/base-model.js +1 -1
  114. package/src/pro/caching/redis-client.js +1 -1
  115. package/src/pro/caching/redis-helper.js +1 -1
  116. package/src/pro/consumers/baseConsumer.js +1 -1
  117. package/src/pro/consumers/declarativeMapper.js +1 -1
  118. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  119. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  120. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  121. package/src/pro/consumers/handlers/index.js +1 -1
  122. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  123. package/src/pro/consumers/index.js +1 -1
  124. package/src/pro/consumers/messageTransformer.js +1 -1
  125. package/src/pro/consumers/validator.js +1 -1
  126. package/src/pro/database/base-model-mysql.js +1 -1
  127. package/src/pro/database/base-model-oracle.js +1 -1
  128. package/src/pro/database/base-model-sqlite.js +1 -1
  129. package/src/pro/database/db-mysql.js +1 -1
  130. package/src/pro/database/db-oracle.js +1 -1
  131. package/src/pro/database/db-sqlite.js +1 -1
  132. package/src/pro/excel/excel-generator.js +1 -1
  133. package/src/pro/excel/excel-parser.js +1 -1
  134. package/src/pro/excel/export-service.js +1 -1
  135. package/src/pro/excel/export_handler.js +1 -1
  136. package/src/pro/excel/import-service.js +1 -1
  137. package/src/pro/excel/import-validator.js +1 -1
  138. package/src/pro/excel/import_handler.js +1 -1
  139. package/src/pro/excel/upsert-builder.js +1 -1
  140. package/src/pro/idgen/idgen-routes.js +1 -1
  141. package/src/pro/integrations/lookup-resolver.js +1 -1
  142. package/src/pro/integrations/upload-handler-v2.js +1 -1
  143. package/src/pro/integrations/upload-handler.js +1 -1
  144. package/src/pro/integrations/webhook.js +1 -1
  145. package/src/pro/locking/lock-routes.js +1 -1
  146. package/src/pro/locking/resource-lock-manager.js +1 -1
  147. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  148. package/src/pro/messaging/kafkaService.js +1 -1
  149. package/src/pro/messaging/messagehubService.js +1 -1
  150. package/src/pro/messaging/rabbitmqService.js +1 -1
  151. package/src/pro/scheduler/job-manager.js +1 -1
  152. package/src/pro/scheduler/job-routes.js +1 -1
  153. package/src/pro/scheduler/job-validator.js +1 -1
  154. package/src/pro/storage/base-storage-provider.js +1 -1
  155. package/src/pro/storage/file-metadata-helper.js +1 -1
  156. package/src/pro/storage/index.js +1 -1
  157. package/src/pro/storage/local-storage-provider.js +1 -1
  158. package/src/pro/storage/s3-storage-provider.js +1 -1
  159. package/src/pro/storage/upload-cleanup-job.js +1 -1
  160. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  161. package/src/pro/storage/upload-pending-tracker.js +1 -1
  162. package/src/pro/websocket/broadcast-helper.js +1 -1
  163. package/src/pro/websocket/index.js +1 -1
  164. package/src/pro/websocket/livesync-server.js +1 -1
  165. package/src/pro/websocket/ws-broadcaster.js +1 -1
  166. package/src/services/export-service.js +1 -1
  167. package/src/services/import-service.js +1 -1
  168. package/src/services/kafkaConsumerService.js +1 -1
  169. package/src/services/kafkaService.js +1 -1
  170. package/src/services/messagehubService.js +1 -1
  171. package/src/services/rabbitmqService.js +1 -1
  172. package/src/utils/cache-invalidation-registry.js +1 -1
  173. package/src/utils/cache-manager.js +1 -1
  174. package/src/utils/component-engine.js +1 -1
  175. package/src/utils/config-extractor.js +1 -1
  176. package/src/utils/consumerLogger.js +1 -1
  177. package/src/utils/context-builder.js +1 -1
  178. package/src/utils/dashboard-helpers.js +1 -1
  179. package/src/utils/dateHelper.js +1 -1
  180. package/src/utils/datetime-formatter.js +1 -1
  181. package/src/utils/datetime-parser.js +1 -1
  182. package/src/utils/db-bootstrap.js +1 -1
  183. package/src/utils/db-mysql.js +1 -1
  184. package/src/utils/db-oracle.js +1 -1
  185. package/src/utils/db-sqlite.js +1 -1
  186. package/src/utils/db.js +1 -1
  187. package/src/utils/demo-generator.js +1 -1
  188. package/src/utils/excel-generator.js +1 -1
  189. package/src/utils/excel-parser.js +1 -1
  190. package/src/utils/file-watcher.js +1 -1
  191. package/src/utils/id-generator.js +1 -1
  192. package/src/utils/idempotency-manager.js +1 -1
  193. package/src/utils/import-validator.js +1 -1
  194. package/src/utils/license-client.js +1 -1
  195. package/src/utils/lock-manager.js +1 -1
  196. package/src/utils/logger.js +1 -1
  197. package/src/utils/lookup-resolver.js +1 -1
  198. package/src/utils/payload-loader.js +1 -1
  199. package/src/utils/processor-response.js +1 -1
  200. package/src/utils/rabbitmq.js +1 -1
  201. package/src/utils/redis-client.js +1 -1
  202. package/src/utils/redis-helper.js +1 -1
  203. package/src/utils/request-scope.js +1 -1
  204. package/src/utils/security-checks.js +1 -1
  205. package/src/utils/service-resolver.js +1 -1
  206. package/src/utils/shutdown-coordinator.js +1 -1
  207. package/src/utils/soft-delete-dashboard-guard.js +1 -1
  208. package/src/utils/sql-table-extractor.js +1 -1
  209. package/src/utils/trusted-keys.js +1 -1
  210. package/src/utils/upload-handler.js +1 -1
  211. package/src/utils/upsert-builder.js +1 -1
  212. package/src/utils/workflow-hook-executor.js +1 -1
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Verifikasi + pencatatan dependency runtime auth extension (Fungsi tambahan,
5
+ * Phase 05, menutup keputusan #6 campaign): middleware butuh `jsonwebtoken`,
6
+ * processor butuh `bcrypt`.
7
+ *
8
+ * Verifikasi resolvability memakai `require.resolve(pkg, { paths: [...] })`
9
+ * persis algoritma resolusi Node — bukan sekadar cek folder `node_modules/`,
10
+ * agar hasilnya benar walau dependency ter-hoist ke level lain.
11
+ *
12
+ * Pencatatan ke `package.json` project bersifat idempoten (tidak menimpa
13
+ * versi yang sudah ada) dan TIDAK pernah menjalankan `npm install`.
14
+ */
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const PLATFORM_PACKAGE_JSON = require('../../../package.json');
20
+
21
+ const REQUIRED_DEPENDENCIES = {
22
+ bcrypt: PLATFORM_PACKAGE_JSON.dependencies.bcrypt,
23
+ jsonwebtoken: PLATFORM_PACKAGE_JSON.dependencies.jsonwebtoken
24
+ };
25
+
26
+ /**
27
+ * @param {string} pkgName
28
+ * @param {string} fromDir
29
+ * @returns {string|null} Resolved entry path, atau null bila tidak resolvable.
30
+ */
31
+ function resolvePackage(pkgName, fromDir) {
32
+ try {
33
+ return require.resolve(pkgName, { paths: [fromDir] });
34
+ } catch (err) {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Tambahkan `bcrypt`/`jsonwebtoken` ke `dependencies` package.json project
41
+ * bila belum ada. Tidak pernah menurunkan/menimpa versi yang sudah di-set.
42
+ *
43
+ * @param {string} workingDir
44
+ * @returns {{ hasPackageJson: boolean, pkgPath: string, updated: boolean, added: string[] }}
45
+ */
46
+ function ensurePackageJsonDependencies(workingDir) {
47
+ const pkgPath = path.join(workingDir, 'package.json');
48
+
49
+ if (!fs.existsSync(pkgPath)) {
50
+ return { hasPackageJson: false, pkgPath, updated: false, added: [] };
51
+ }
52
+
53
+ const raw = fs.readFileSync(pkgPath, 'utf8');
54
+ let pkg;
55
+ try {
56
+ pkg = JSON.parse(raw);
57
+ } catch (err) {
58
+ throw new Error(`Gagal parse ${pkgPath}: ${err.message}`);
59
+ }
60
+
61
+ pkg.dependencies = pkg.dependencies || {};
62
+ const added = [];
63
+
64
+ for (const [name, version] of Object.entries(REQUIRED_DEPENDENCIES)) {
65
+ if (!Object.prototype.hasOwnProperty.call(pkg.dependencies, name)) {
66
+ pkg.dependencies[name] = version;
67
+ added.push(name);
68
+ }
69
+ }
70
+
71
+ if (added.length > 0) {
72
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
73
+ }
74
+
75
+ return { hasPackageJson: true, pkgPath, updated: added.length > 0, added };
76
+ }
77
+
78
+ /**
79
+ * @param {{ workingDir?: string }} options
80
+ * @returns {{
81
+ * resolution: Record<string, { resolvable: boolean, resolvedPath: string|null }>,
82
+ * allResolvable: boolean,
83
+ * packageJson: { hasPackageJson: boolean, pkgPath: string, updated: boolean, added: string[] },
84
+ * needsInstallInstruction: boolean
85
+ * }}
86
+ */
87
+ function ensureAuthDependencies({ workingDir = process.cwd() } = {}) {
88
+ const resolution = {};
89
+ for (const name of Object.keys(REQUIRED_DEPENDENCIES)) {
90
+ const resolvedPath = resolvePackage(name, workingDir);
91
+ resolution[name] = { resolvable: Boolean(resolvedPath), resolvedPath };
92
+ }
93
+ const allResolvable = Object.values(resolution).every((r) => r.resolvable);
94
+
95
+ const packageJson = ensurePackageJsonDependencies(workingDir);
96
+
97
+ const needsInstallInstruction = !allResolvable || !packageJson.hasPackageJson;
98
+
99
+ return { resolution, allResolvable, packageJson, needsInstallInstruction };
100
+ }
101
+
102
+ module.exports = { ensureAuthDependencies, REQUIRED_DEPENDENCIES, resolvePackage };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Injeksi variabel env auth (Fungsi tambahan, Phase 05) ke file `--config`,
5
+ * meniru blok Auth/JWT pada prototype `myapp-with-auth/config/db-connection.env`.
6
+ * Idempoten: hanya menambahkan key yang belum ada di file; tidak pernah
7
+ * menimpa nilai existing (termasuk JWT_SECRET yang sudah di-set user).
8
+ *
9
+ * Resolusi path config memakai `resolveConfig()` yang sama dipakai
10
+ * migrate-runner.js, agar blok Auth/JWT ditambahkan ke file yang sama dengan
11
+ * yang dipakai langkah migrate (cascade lookup cwd -> config/ -> +.env).
12
+ */
13
+
14
+ const crypto = require('node:crypto');
15
+ const path = require('node:path');
16
+
17
+ const { readEnvFile, writeEnvFile } = require('../utils/env-manager');
18
+ const { resolveConfig } = require('../utils/config-resolver');
19
+
20
+ const AUTH_ENV_DEFAULTS = {
21
+ JWT_ALGORITHM: 'HS256',
22
+ ACCESS_TOKEN_EXPIRY_MIN: '60',
23
+ REFRESH_TOKEN_EXPIRY_DAYS: '30',
24
+ GOOGLE_CLIENT_ID: ''
25
+ };
26
+
27
+ const AUTH_ENV_KEYS = [
28
+ 'JWT_SECRET',
29
+ 'JWT_ALGORITHM',
30
+ 'ACCESS_TOKEN_EXPIRY_MIN',
31
+ 'REFRESH_TOKEN_EXPIRY_DAYS',
32
+ 'GOOGLE_CLIENT_ID'
33
+ ];
34
+
35
+ const AUTH_ENV_HEADER_COMMENT = '# Auth / JWT Configuration (rfx_auth, generated by "project auth")';
36
+
37
+ function generateJwtSecret() {
38
+ return crypto.randomBytes(48).toString('hex');
39
+ }
40
+
41
+ /**
42
+ * @param {{ configPath: string, workingDir?: string }} options
43
+ * @returns {{
44
+ * filePath: string,
45
+ * added: string[],
46
+ * skipped: string[],
47
+ * jwtSecretGenerated: boolean
48
+ * }}
49
+ */
50
+ function injectAuthEnv({ configPath, workingDir = process.cwd() } = {}) {
51
+ const resolved = resolveConfig(configPath, workingDir);
52
+ const filePath = resolved ? resolved.path : path.resolve(workingDir, configPath || 'config/db-connection.env');
53
+
54
+ const { data, lines } = readEnvFile(filePath);
55
+ const added = [];
56
+ const skipped = [];
57
+
58
+ for (const key of AUTH_ENV_KEYS) {
59
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
60
+ skipped.push(key);
61
+ continue;
62
+ }
63
+
64
+ data[key] = key === 'JWT_SECRET' ? generateJwtSecret() : AUTH_ENV_DEFAULTS[key];
65
+ added.push(key);
66
+ }
67
+
68
+ if (added.length > 0) {
69
+ const nextLines = lines.length > 0 ? [...lines, '', AUTH_ENV_HEADER_COMMENT] : [AUTH_ENV_HEADER_COMMENT];
70
+ writeEnvFile(filePath, data, nextLines);
71
+ }
72
+
73
+ return {
74
+ filePath,
75
+ added,
76
+ skipped,
77
+ jwtSecretGenerated: added.includes('JWT_SECRET')
78
+ };
79
+ }
80
+
81
+ module.exports = { injectAuthEnv, AUTH_ENV_KEYS };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migrate runner auth extension (Fungsi 2). Memuat kedua SDF auth (`rfx_auth_user`
5
+ * & `rfx_auth_refresh_token`) dari `--schema-path`, memvalidasi cross-model (FK
6
+ * refresh_token -> user), men-generate DDL, lalu apply ke DB via primitif
7
+ * `dbschema-kit` langsung (in-process). TIDAK menyentuh `generators/cli/schema/migrate.js`
8
+ * (keputusan #3 campaign project-auth-command-v1) — primitif dipanggil ulang di sini.
9
+ *
10
+ * Test seam: sama seperti migrate.js, executor dimuat lewat loadApplyExecutor()
11
+ * yang menghormati DBSCHEMA_KIT_TEST_APPLY_STUB.
12
+ */
13
+
14
+ const path = require('path');
15
+
16
+ const { loadSchemaPath } = require('../dbschema-kit/loader');
17
+ const { validateCrossModel } = require('../dbschema-kit/validator/cross-model-validator');
18
+ const { generateDDL } = require('../dbschema-kit/ddl-generator');
19
+ const { loadConfig } = require('../dbschema-kit/connection');
20
+ const { splitStatements } = require('../dbschema-kit/statement-splitter');
21
+ const { applyIfNotExistsModifier } = require('../dbschema-kit/statement-modifier');
22
+ const { resolveConfig } = require('../utils/config-resolver');
23
+ const { AUTH_USER_TABLE, AUTH_REFRESH_TOKEN_TABLE } = require('./prefix');
24
+
25
+ function loadApplyExecutor() {
26
+ const stubPath = process.env.DBSCHEMA_KIT_TEST_APPLY_STUB;
27
+ if (stubPath) {
28
+ return require(path.resolve(stubPath));
29
+ }
30
+ return require('../dbschema-kit/apply-executor');
31
+ }
32
+
33
+ /**
34
+ * Muat kedua SDF auth dari `schemaPath` ke satu Map gabungan, agar
35
+ * validateCrossModel bisa memverifikasi FK lintas model.
36
+ *
37
+ * @param {string} schemaPath
38
+ * @returns {Map<string, object>}
39
+ */
40
+ function loadAuthModels(schemaPath) {
41
+ const models = new Map();
42
+ for (const tableName of [AUTH_USER_TABLE, AUTH_REFRESH_TOKEN_TABLE]) {
43
+ const filePath = path.join(schemaPath, `${tableName}.js`);
44
+ const fileModels = loadSchemaPath(filePath);
45
+ for (const [qualified, ir] of fileModels) {
46
+ models.set(qualified, ir);
47
+ }
48
+ }
49
+ return models;
50
+ }
51
+
52
+ /**
53
+ * @param {{ schemaPath: string, configPath: string }} options
54
+ * @returns {Promise<{ tables: string[], dialect: string, statementsApplied: number, result: object }>}
55
+ */
56
+ async function runAuthMigrate({ schemaPath, configPath }) {
57
+ const resolved = resolveConfig(configPath, process.cwd());
58
+ if (!resolved) {
59
+ throw new Error(
60
+ 'Migrate auth gagal: --config=<file> tidak ditemukan. Set default config ' +
61
+ "('npx restforge config set-default --config=<file>') atau sediakan --config eksplisit."
62
+ );
63
+ }
64
+
65
+ let config;
66
+ try {
67
+ config = loadConfig(resolved.path);
68
+ } catch (err) {
69
+ throw new Error(`Migrate auth gagal memuat config DB (${resolved.path}): ${err.message}`);
70
+ }
71
+
72
+ let models;
73
+ try {
74
+ models = loadAuthModels(schemaPath);
75
+ } catch (err) {
76
+ throw new Error(`Migrate auth gagal memuat SDF auth dari '${schemaPath}': ${err.message}`);
77
+ }
78
+
79
+ const crossModelIssues = validateCrossModel(models);
80
+ const crossModelErrors = crossModelIssues.filter((issue) => issue.severity === 'error');
81
+ if (crossModelErrors.length > 0) {
82
+ const messages = crossModelErrors.map((issue) => issue.message).join('; ');
83
+ throw new Error(`Migrate auth gagal: validasi cross-model menemukan error: ${messages}`);
84
+ }
85
+
86
+ const ddl = generateDDL(models, { dialect: config.dialect, drop: false });
87
+ const statements = splitStatements(ddl, config.dialect)
88
+ .map((statement) => applyIfNotExistsModifier(statement, config.dialect));
89
+
90
+ const executor = loadApplyExecutor();
91
+
92
+ let result;
93
+ try {
94
+ result = await executor.applyStatements({ statements, dialect: config.dialect, config });
95
+ } catch (err) {
96
+ throw new Error(`Migrate auth gagal menerapkan tabel ke database: ${err.message}`);
97
+ }
98
+
99
+ if (!result || result.status !== 'SUCCESS') {
100
+ throw new Error(`Migrate auth tidak SUCCESS (status: ${result ? result.status : 'UNKNOWN'}).`);
101
+ }
102
+
103
+ return {
104
+ tables: [AUTH_USER_TABLE, AUTH_REFRESH_TOKEN_TABLE],
105
+ dialect: config.dialect,
106
+ statementsApplied: statements.length,
107
+ result
108
+ };
109
+ }
110
+
111
+ module.exports = { runAuthMigrate, loadAuthModels };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Konstanta prefix `rfx` (RestForge eXtension) untuk seluruh artefak auth
5
+ * extension. Satu sumber dipakai ulang oleh Phase 01 (SDF), Phase 02
6
+ * (tabel hasil migrate), dan Phase 03-04 (component/router/processor).
7
+ */
8
+
9
+ const PREFIX = 'rfx';
10
+
11
+ const AUTH_USER_TABLE = `${PREFIX}_auth_user`;
12
+ const AUTH_REFRESH_TOKEN_TABLE = `${PREFIX}_auth_refresh_token`;
13
+ const AUTH_MIDDLEWARE_NAME = `${PREFIX}_auth-middleware`;
14
+ const AUTH_ROUTER_NAME = `${PREFIX}_auth`;
15
+
16
+ module.exports = {
17
+ PREFIX,
18
+ AUTH_USER_TABLE,
19
+ AUTH_REFRESH_TOKEN_TABLE,
20
+ AUTH_MIDDLEWARE_NAME,
21
+ AUTH_ROUTER_NAME
22
+ };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Generator processor auth (Fungsi 3b). Merender aset template di
5
+ * `templates/processor/` via template-renderer (Phase 03), lalu menulis ke
6
+ * lokasi target memakai writeFileWithBackup — perilaku force/skip identik
7
+ * Fungsi 1/3a: tanpa --force file existing di-skip, dengan --force
8
+ * di-overwrite + backup.
9
+ *
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.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const { renderTemplate } = require('./template-renderer');
19
+ const FileUtils = require('../utils/file-utils');
20
+ const { AUTH_USER_TABLE, AUTH_REFRESH_TOKEN_TABLE, AUTH_MIDDLEWARE_NAME } = require('./prefix');
21
+
22
+ const TEMPLATES_DIR = path.join(__dirname, 'templates', 'processor');
23
+
24
+ const PROCESSOR_NAMES = ['register', 'login', 'google', 'refresh', 'logout', 'me', 'reset-password'];
25
+
26
+ const PARAMS = {
27
+ AUTH_USER_TABLE,
28
+ AUTH_REFRESH_TOKEN_TABLE,
29
+ AUTH_MIDDLEWARE_NAME
30
+ };
31
+
32
+ /**
33
+ * @param {{ processorDir: string, force?: boolean }} options
34
+ * @returns {{ written: string[], skipped: string[] }}
35
+ */
36
+ function generateAuthProcessors({ processorDir, force = false }) {
37
+ const written = [];
38
+ const skipped = [];
39
+
40
+ for (const name of PROCESSOR_NAMES) {
41
+ const targetPath = path.join(processorDir, `${name}.js`);
42
+
43
+ if (fs.existsSync(targetPath) && !force) {
44
+ skipped.push(targetPath);
45
+ continue;
46
+ }
47
+
48
+ const templatePath = path.join(TEMPLATES_DIR, `${name}.js.tmpl`);
49
+ const content = renderTemplate(templatePath, PARAMS);
50
+ FileUtils.writeFileWithBackup(targetPath, content, true);
51
+ written.push(targetPath);
52
+ }
53
+
54
+ return { written, skipped };
55
+ }
56
+
57
+ module.exports = { generateAuthProcessors };
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Generator SDF auth (prefix `rfx`). Membangun IR dua model auth via
5
+ * `defineModel` (dbschema-kit), men-serialize ke source SDF via `serialize()`,
6
+ * lalu menulis ke `<schema-path>/<table>.js` (skip/backup sesuai `--force`).
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const { defineModel } = require('../dbschema-kit/define-model');
13
+ const { serialize } = require('../dbschema-kit/schema-printer');
14
+ const FileUtils = require('../utils/file-utils');
15
+ const { AUTH_USER_TABLE, AUTH_REFRESH_TOKEN_TABLE } = require('./prefix');
16
+
17
+ function buildAuthUserModel() {
18
+ return defineModel(AUTH_USER_TABLE, {
19
+ schema: 'public',
20
+ fields: {
21
+ user_id: 'string:36 pk',
22
+ username: 'string:100 notnull unique',
23
+ email: 'string:255',
24
+ full_name: 'string:150',
25
+ password_hash: 'string:255 notnull',
26
+ is_active: 'boolean notnull default:true',
27
+ is_locked: 'boolean notnull default:false',
28
+ failed_login_count: 'integer notnull default:0',
29
+ last_login_at: 'timestamp',
30
+ password_changed_at: 'timestamp',
31
+ created_at: 'timestamp default:now()',
32
+ created_by: 'string:70',
33
+ updated_at: 'timestamp',
34
+ updated_by: 'string:70'
35
+ },
36
+ indexes: ['username', 'email', 'is_active']
37
+ });
38
+ }
39
+
40
+ function buildAuthRefreshTokenModel() {
41
+ return defineModel(AUTH_REFRESH_TOKEN_TABLE, {
42
+ schema: 'public',
43
+ fields: {
44
+ token_id: 'string:36 pk',
45
+ user_id: 'string:36 notnull',
46
+ token_hash: 'string:255 notnull',
47
+ is_revoked: 'boolean notnull default:false',
48
+ expires_at: 'timestamp notnull',
49
+ created_at: 'timestamp default:now()'
50
+ },
51
+ indexes: ['user_id', 'expires_at'],
52
+ relations: {
53
+ user: {
54
+ type: 'belongsTo',
55
+ target: AUTH_USER_TABLE,
56
+ localKey: 'user_id',
57
+ references: 'user_id',
58
+ onDelete: 'cascade',
59
+ onUpdate: 'restrict'
60
+ }
61
+ }
62
+ });
63
+ }
64
+
65
+ function buildAuthModels() {
66
+ return {
67
+ [AUTH_USER_TABLE]: buildAuthUserModel(),
68
+ [AUTH_REFRESH_TOKEN_TABLE]: buildAuthRefreshTokenModel()
69
+ };
70
+ }
71
+
72
+ /**
73
+ * @param {{ schemaPath: string, force?: boolean }} options
74
+ * @returns {{ written: string[], skipped: string[] }}
75
+ */
76
+ function generateAuthSdf({ schemaPath, force = false }) {
77
+ const models = buildAuthModels();
78
+ const written = [];
79
+ const skipped = [];
80
+
81
+ for (const [tableName, ir] of Object.entries(models)) {
82
+ const targetPath = path.join(schemaPath, `${tableName}.js`);
83
+
84
+ if (fs.existsSync(targetPath) && !force) {
85
+ skipped.push(targetPath);
86
+ continue;
87
+ }
88
+
89
+ const source = serialize(ir, { generatedBy: 'npx restforge project auth --create' });
90
+ FileUtils.writeFileWithBackup(targetPath, source, true);
91
+ written.push(targetPath);
92
+ }
93
+
94
+ return { written, skipped };
95
+ }
96
+
97
+ module.exports = {
98
+ buildAuthUserModel,
99
+ buildAuthRefreshTokenModel,
100
+ buildAuthModels,
101
+ generateAuthSdf
102
+ };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Renderer template-asset sederhana untuk artefak auth extension. Membaca file
5
+ * `.tmpl` lalu mengganti placeholder `{{KEY}}` dengan value dari `params`.
6
+ * Dipakai Fungsi 3a (component/router) dan akan dipakai ulang Fungsi 3b
7
+ * (6 processor, Phase 04).
8
+ */
9
+
10
+ const fs = require('fs');
11
+
12
+ /**
13
+ * @param {string} templatePath - Path absolut ke file `.tmpl`
14
+ * @param {Object<string, string>} [params] - Map placeholder -> value pengganti
15
+ * @returns {string} isi template setelah substitusi placeholder
16
+ */
17
+ function renderTemplate(templatePath, params = {}) {
18
+ // Normalisasi CRLF -> LF agar output deterministik lintas-platform. File
19
+ // `.tmpl` bisa ter-checkout sebagai CRLF di Windows (`.gitattributes`
20
+ // `text=auto`) atau ikut terbawa saat npm pack; tanpa normalisasi, output
21
+ // generator akan ber-CRLF dan menyimpang dari prototype acuan yang ber-LF.
22
+ let content = fs.readFileSync(templatePath, 'utf8').replace(/\r\n/g, '\n');
23
+ for (const [key, value] of Object.entries(params)) {
24
+ content = content.split(`{{${key}}}`).join(value);
25
+ }
26
+ return content;
27
+ }
28
+
29
+ module.exports = { renderTemplate };
@@ -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
+ };