@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,152 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Processor: login
5
+ * Verifikasi kredensial, lalu terbitkan JWT access token + refresh token.
6
+ * Method: POST
7
+ */
8
+
9
+ const bcrypt = require('bcrypt');
10
+ const crypto = require('crypto');
11
+ const {
12
+ generateAccessToken,
13
+ getAccessTokenExpiryMin
14
+ } = require('../../../../components/handlers/{{AUTH_MIDDLEWARE_NAME}}');
15
+
16
+ const BCRYPT_ROUNDS = 12;
17
+ const MAX_FAILED_LOGIN = 5;
18
+
19
+ function getRefreshExpiryDays() {
20
+ return parseInt(process.env.REFRESH_TOKEN_EXPIRY_DAYS || '30', 10);
21
+ }
22
+
23
+ module.exports = {
24
+ async process(input, services, req) {
25
+ const { db, logger } = services;
26
+
27
+ try {
28
+ const { username, password } = input;
29
+
30
+ if (!username || !password) {
31
+ return {
32
+ success: false,
33
+ statusCode: 400,
34
+ message: 'Username and password are required.',
35
+ timestamp: new Date().toISOString()
36
+ };
37
+ }
38
+
39
+ const userRows = await db.executeQuery(
40
+ `SELECT user_id, username, email, full_name,
41
+ password_hash, is_active, is_locked, failed_login_count
42
+ FROM public.{{AUTH_USER_TABLE}}
43
+ WHERE username = $1`,
44
+ [username]
45
+ );
46
+ const user = userRows[0] || null;
47
+
48
+ if (!user) {
49
+ return {
50
+ success: false,
51
+ statusCode: 401,
52
+ message: 'Invalid username or password.',
53
+ timestamp: new Date().toISOString()
54
+ };
55
+ }
56
+
57
+ if (!user.is_active) {
58
+ return {
59
+ success: false,
60
+ statusCode: 403,
61
+ message: 'Account is inactive. Contact administrator.',
62
+ timestamp: new Date().toISOString()
63
+ };
64
+ }
65
+
66
+ if (user.is_locked) {
67
+ return {
68
+ success: false,
69
+ statusCode: 403,
70
+ message: 'Account locked due to too many failed login attempts. Contact administrator.',
71
+ timestamp: new Date().toISOString()
72
+ };
73
+ }
74
+
75
+ const passwordValid = await bcrypt.compare(password, user.password_hash);
76
+
77
+ if (!passwordValid) {
78
+ const newFailedCount = (user.failed_login_count || 0) + 1;
79
+ const shouldLock = newFailedCount >= MAX_FAILED_LOGIN;
80
+
81
+ await db.executeQuery(
82
+ `UPDATE public.{{AUTH_USER_TABLE}}
83
+ SET failed_login_count = $1, is_locked = $2, updated_at = NOW()
84
+ WHERE user_id = $3`,
85
+ [newFailedCount, shouldLock, user.user_id]
86
+ );
87
+
88
+ const message = shouldLock
89
+ ? 'Account locked due to too many failed login attempts. Contact administrator.'
90
+ : `Invalid username or password. Attempts remaining: ${MAX_FAILED_LOGIN - newFailedCount}.`;
91
+
92
+ return {
93
+ success: false,
94
+ statusCode: 401,
95
+ message,
96
+ timestamp: new Date().toISOString()
97
+ };
98
+ }
99
+
100
+ // Login berhasil — reset counter, update last_login_at
101
+ await db.executeQuery(
102
+ `UPDATE public.{{AUTH_USER_TABLE}}
103
+ SET failed_login_count = 0, last_login_at = NOW(), updated_at = NOW()
104
+ WHERE user_id = $1`,
105
+ [user.user_id]
106
+ );
107
+
108
+ const accessToken = generateAccessToken(user);
109
+ const expiresIn = getAccessTokenExpiryMin() * 60;
110
+
111
+ // Refresh token: `<user_id>:<random-hex>`, disimpan sebagai hash bcrypt
112
+ const refreshTokenRaw = `${user.user_id}:${crypto.randomBytes(64).toString('hex')}`;
113
+ const refreshTokenHash = await bcrypt.hash(refreshTokenRaw, BCRYPT_ROUNDS);
114
+ const refreshExpiresAt = new Date();
115
+ refreshExpiresAt.setDate(refreshExpiresAt.getDate() + getRefreshExpiryDays());
116
+
117
+ await db.executeQuery(
118
+ `INSERT INTO public.{{AUTH_REFRESH_TOKEN_TABLE}}
119
+ (token_id, user_id, token_hash, expires_at, created_at)
120
+ VALUES ($1, $2, $3, $4, NOW())`,
121
+ [crypto.randomUUID(), user.user_id, refreshTokenHash, refreshExpiresAt]
122
+ );
123
+
124
+ return {
125
+ success: true,
126
+ statusCode: 200,
127
+ message: 'Login successful.',
128
+ data: {
129
+ access_token: accessToken,
130
+ refresh_token: refreshTokenRaw,
131
+ token_type: 'Bearer',
132
+ expires_in: expiresIn,
133
+ user: {
134
+ user_id: user.user_id,
135
+ username: user.username,
136
+ email: user.email,
137
+ full_name: user.full_name
138
+ }
139
+ },
140
+ timestamp: new Date().toISOString()
141
+ };
142
+ } catch (error) {
143
+ logger.error({ error: error.message }, '[auth-login] Unexpected error');
144
+ return {
145
+ success: false,
146
+ statusCode: 500,
147
+ message: 'An internal server error occurred.',
148
+ timestamp: new Date().toISOString()
149
+ };
150
+ }
151
+ }
152
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Processor: logout
5
+ * Revoke refresh token bila disertakan. Selalu sukses (idempoten dari sisi client).
6
+ * Method: POST
7
+ */
8
+
9
+ const bcrypt = require('bcrypt');
10
+
11
+ module.exports = {
12
+ async process(input, services, req) {
13
+ const { db, logger } = services;
14
+
15
+ try {
16
+ const { refresh_token } = input;
17
+
18
+ if (refresh_token) {
19
+ const colonIdx = refresh_token.indexOf(':');
20
+ if (colonIdx >= 0) {
21
+ const userId = refresh_token.substring(0, colonIdx);
22
+
23
+ const tokenRows = await db.executeQuery(
24
+ `SELECT token_id, token_hash
25
+ FROM public.{{AUTH_REFRESH_TOKEN_TABLE}}
26
+ WHERE user_id = $1 AND is_revoked = FALSE`,
27
+ [userId]
28
+ );
29
+
30
+ for (const t of (tokenRows || [])) {
31
+ if (await bcrypt.compare(refresh_token, t.token_hash)) {
32
+ await db.executeQuery(
33
+ 'UPDATE public.{{AUTH_REFRESH_TOKEN_TABLE}} SET is_revoked = TRUE WHERE token_id = $1',
34
+ [t.token_id]
35
+ );
36
+ break;
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ return {
43
+ success: true,
44
+ statusCode: 200,
45
+ message: 'Logout successful.',
46
+ timestamp: new Date().toISOString()
47
+ };
48
+ } catch (error) {
49
+ logger.error({ error: error.message }, '[auth-logout] Unexpected error');
50
+ return {
51
+ success: false,
52
+ statusCode: 500,
53
+ message: 'An internal server error occurred.',
54
+ timestamp: new Date().toISOString()
55
+ };
56
+ }
57
+ }
58
+ };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Processor: me
5
+ * Mengembalikan profil user dari JWT access token (header Authorization).
6
+ * Method: GET
7
+ */
8
+
9
+ const { verifyToken } = require('../../../../components/handlers/{{AUTH_MIDDLEWARE_NAME}}');
10
+
11
+ module.exports = {
12
+ async process(input, services, req) {
13
+ const { db, logger } = services;
14
+
15
+ try {
16
+ let decoded;
17
+ try {
18
+ decoded = verifyToken(req);
19
+ } catch (err) {
20
+ return {
21
+ success: false,
22
+ statusCode: err.statusCode || 401,
23
+ message: err.message,
24
+ timestamp: new Date().toISOString()
25
+ };
26
+ }
27
+
28
+ const userRows = await db.executeQuery(
29
+ `SELECT user_id, username, email, full_name,
30
+ is_active, is_locked, last_login_at,
31
+ created_at, updated_at
32
+ FROM public.{{AUTH_USER_TABLE}}
33
+ WHERE user_id = $1`,
34
+ [decoded.sub]
35
+ );
36
+ const user = userRows[0] || null;
37
+
38
+ if (!user) {
39
+ return {
40
+ success: false,
41
+ statusCode: 404,
42
+ message: 'User not found.',
43
+ timestamp: new Date().toISOString()
44
+ };
45
+ }
46
+
47
+ return {
48
+ success: true,
49
+ statusCode: 200,
50
+ message: 'OK',
51
+ data: user,
52
+ timestamp: new Date().toISOString()
53
+ };
54
+ } catch (error) {
55
+ logger.error({ error: error.message }, '[auth-me] Unexpected error');
56
+ return {
57
+ success: false,
58
+ statusCode: 500,
59
+ message: 'An internal server error occurred.',
60
+ timestamp: new Date().toISOString()
61
+ };
62
+ }
63
+ }
64
+ };
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Processor: refresh
5
+ * Tukar refresh token dengan access token baru (dengan token rotation:
6
+ * refresh token lama di-revoke, refresh token baru diterbitkan).
7
+ * Method: POST
8
+ */
9
+
10
+ const bcrypt = require('bcrypt');
11
+ const crypto = require('crypto');
12
+ const {
13
+ generateAccessToken,
14
+ getAccessTokenExpiryMin
15
+ } = require('../../../../components/handlers/{{AUTH_MIDDLEWARE_NAME}}');
16
+
17
+ const BCRYPT_ROUNDS = 12;
18
+
19
+ function getRefreshExpiryDays() {
20
+ return parseInt(process.env.REFRESH_TOKEN_EXPIRY_DAYS || '30', 10);
21
+ }
22
+
23
+ module.exports = {
24
+ async process(input, services, req) {
25
+ const { db, logger } = services;
26
+
27
+ try {
28
+ const { refresh_token } = input;
29
+
30
+ if (!refresh_token) {
31
+ return {
32
+ success: false,
33
+ statusCode: 400,
34
+ message: 'refresh_token is required.',
35
+ timestamp: new Date().toISOString()
36
+ };
37
+ }
38
+
39
+ const colonIdx = refresh_token.indexOf(':');
40
+ if (colonIdx < 0) {
41
+ return {
42
+ success: false,
43
+ statusCode: 401,
44
+ message: 'Invalid refresh token.',
45
+ timestamp: new Date().toISOString()
46
+ };
47
+ }
48
+ const userId = refresh_token.substring(0, colonIdx);
49
+
50
+ const tokenRows = await db.executeQuery(
51
+ `SELECT token_id, token_hash
52
+ FROM public.{{AUTH_REFRESH_TOKEN_TABLE}}
53
+ WHERE user_id = $1 AND is_revoked = FALSE AND expires_at > NOW()`,
54
+ [userId]
55
+ );
56
+
57
+ let matched = null;
58
+ for (const t of (tokenRows || [])) {
59
+ if (await bcrypt.compare(refresh_token, t.token_hash)) {
60
+ matched = t;
61
+ break;
62
+ }
63
+ }
64
+
65
+ if (!matched) {
66
+ return {
67
+ success: false,
68
+ statusCode: 401,
69
+ message: 'Invalid or expired refresh token.',
70
+ timestamp: new Date().toISOString()
71
+ };
72
+ }
73
+
74
+ const userRows = await db.executeQuery(
75
+ `SELECT user_id, username, email, full_name, is_active, is_locked
76
+ FROM public.{{AUTH_USER_TABLE}}
77
+ WHERE user_id = $1`,
78
+ [userId]
79
+ );
80
+ const user = userRows[0] || null;
81
+
82
+ if (!user || !user.is_active || user.is_locked) {
83
+ return {
84
+ success: false,
85
+ statusCode: 403,
86
+ message: 'User account is inactive or locked.',
87
+ timestamp: new Date().toISOString()
88
+ };
89
+ }
90
+
91
+ // Rotation: revoke token lama
92
+ await db.executeQuery(
93
+ 'UPDATE public.{{AUTH_REFRESH_TOKEN_TABLE}} SET is_revoked = TRUE WHERE token_id = $1',
94
+ [matched.token_id]
95
+ );
96
+
97
+ const accessToken = generateAccessToken(user);
98
+ const expiresIn = getAccessTokenExpiryMin() * 60;
99
+
100
+ const newRefreshRaw = `${user.user_id}:${crypto.randomBytes(64).toString('hex')}`;
101
+ const newRefreshHash = await bcrypt.hash(newRefreshRaw, BCRYPT_ROUNDS);
102
+ const refreshExpiresAt = new Date();
103
+ refreshExpiresAt.setDate(refreshExpiresAt.getDate() + getRefreshExpiryDays());
104
+
105
+ await db.executeQuery(
106
+ `INSERT INTO public.{{AUTH_REFRESH_TOKEN_TABLE}}
107
+ (token_id, user_id, token_hash, expires_at, created_at)
108
+ VALUES ($1, $2, $3, $4, NOW())`,
109
+ [crypto.randomUUID(), user.user_id, newRefreshHash, refreshExpiresAt]
110
+ );
111
+
112
+ return {
113
+ success: true,
114
+ statusCode: 200,
115
+ message: 'Token refreshed successfully.',
116
+ data: {
117
+ access_token: accessToken,
118
+ refresh_token: newRefreshRaw,
119
+ token_type: 'Bearer',
120
+ expires_in: expiresIn
121
+ },
122
+ timestamp: new Date().toISOString()
123
+ };
124
+ } catch (error) {
125
+ logger.error({ error: error.message }, '[auth-refresh] Unexpected error');
126
+ return {
127
+ success: false,
128
+ statusCode: 500,
129
+ message: 'An internal server error occurred.',
130
+ timestamp: new Date().toISOString()
131
+ };
132
+ }
133
+ }
134
+ };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Processor: register
5
+ * Membuat user baru dengan password ter-hash bcrypt.
6
+ * Method: POST
7
+ */
8
+
9
+ const bcrypt = require('bcrypt');
10
+ const crypto = require('crypto');
11
+
12
+ const BCRYPT_ROUNDS = 12;
13
+
14
+ module.exports = {
15
+ async process(input, services, req) {
16
+ const { db, logger } = services;
17
+
18
+ try {
19
+ const { username, password, email, full_name } = input;
20
+
21
+ if (!username || !password) {
22
+ return {
23
+ success: false,
24
+ statusCode: 400,
25
+ message: 'Username and password are required.',
26
+ timestamp: new Date().toISOString()
27
+ };
28
+ }
29
+
30
+ const existing = await db.executeQuery(
31
+ 'SELECT user_id FROM public.{{AUTH_USER_TABLE}} WHERE username = $1',
32
+ [username]
33
+ );
34
+ if (existing && existing.length > 0) {
35
+ return {
36
+ success: false,
37
+ statusCode: 409,
38
+ message: 'Username is already taken.',
39
+ timestamp: new Date().toISOString()
40
+ };
41
+ }
42
+
43
+ const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS);
44
+ const userId = crypto.randomUUID();
45
+
46
+ await db.executeQuery(
47
+ `INSERT INTO public.{{AUTH_USER_TABLE}}
48
+ (user_id, username, email, full_name, password_hash,
49
+ is_active, is_locked, failed_login_count,
50
+ password_changed_at, created_at)
51
+ VALUES ($1, $2, $3, $4, $5, TRUE, FALSE, 0, NOW(), NOW())`,
52
+ [userId, username, email || null, full_name || null, passwordHash]
53
+ );
54
+
55
+ return {
56
+ success: true,
57
+ statusCode: 201,
58
+ message: 'Registration successful.',
59
+ data: {
60
+ user_id: userId,
61
+ username,
62
+ email: email || null,
63
+ full_name: full_name || null
64
+ },
65
+ timestamp: new Date().toISOString()
66
+ };
67
+ } catch (error) {
68
+ logger.error({ error: error.message }, '[auth-register] Unexpected error');
69
+ return {
70
+ success: false,
71
+ statusCode: 500,
72
+ message: 'An internal server error occurred.',
73
+ timestamp: new Date().toISOString()
74
+ };
75
+ }
76
+ }
77
+ };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Processor: reset-password (prototype, tanpa verifikasi email)
5
+ * Reset password berdasarkan email saja, tanpa token/link verifikasi.
6
+ * Method: POST body: { email, new_password, confirm_password }
7
+ *
8
+ * Catatan keamanan: tanpa verifikasi kepemilikan email, siapa pun yang tahu
9
+ * email terdaftar dapat mengganti password. Hanya layak untuk prototype lokal.
10
+ */
11
+
12
+ const bcrypt = require('bcrypt');
13
+
14
+ const BCRYPT_ROUNDS = 12;
15
+ const MIN_PASSWORD = 6;
16
+
17
+ module.exports = {
18
+ async process(input, services, req) {
19
+ const { db, logger } = services;
20
+
21
+ try {
22
+ const { email, new_password, confirm_password } = input;
23
+
24
+ if (!email || !new_password || !confirm_password) {
25
+ return {
26
+ success: false,
27
+ statusCode: 400,
28
+ message: 'Email, new password, and confirmation are required.',
29
+ timestamp: new Date().toISOString()
30
+ };
31
+ }
32
+ if (new_password.length < MIN_PASSWORD) {
33
+ return {
34
+ success: false,
35
+ statusCode: 400,
36
+ message: `Password must be at least ${MIN_PASSWORD} characters.`,
37
+ timestamp: new Date().toISOString()
38
+ };
39
+ }
40
+ if (new_password !== confirm_password) {
41
+ return {
42
+ success: false,
43
+ statusCode: 400,
44
+ message: 'Password confirmation does not match.',
45
+ timestamp: new Date().toISOString()
46
+ };
47
+ }
48
+
49
+ const rows = await db.executeQuery(
50
+ 'SELECT user_id FROM public.{{AUTH_USER_TABLE}} WHERE email = $1',
51
+ [email]
52
+ );
53
+
54
+ if (!rows || rows.length === 0) {
55
+ return {
56
+ success: false,
57
+ statusCode: 404,
58
+ message: 'Email is not registered.',
59
+ timestamp: new Date().toISOString()
60
+ };
61
+ }
62
+ if (rows.length > 1) {
63
+ // Email tidak unik di schema; tolak daripada diam-diam mereset banyak akun.
64
+ return {
65
+ success: false,
66
+ statusCode: 409,
67
+ message: 'Email is registered to multiple accounts. Contact administrator.',
68
+ timestamp: new Date().toISOString()
69
+ };
70
+ }
71
+
72
+ const userId = rows[0].user_id;
73
+ const passwordHash = await bcrypt.hash(new_password, BCRYPT_ROUNDS);
74
+
75
+ // Reset password sekaligus buka kunci akun dan reset hitungan gagal login.
76
+ await db.executeQuery(
77
+ `UPDATE public.{{AUTH_USER_TABLE}}
78
+ SET password_hash = $1, password_changed_at = NOW(), updated_at = NOW(),
79
+ failed_login_count = 0, is_locked = FALSE
80
+ WHERE user_id = $2`,
81
+ [passwordHash, userId]
82
+ );
83
+
84
+ // Revoke semua refresh token lama: paksa login ulang di seluruh sesi.
85
+ await db.executeQuery(
86
+ 'UPDATE public.{{AUTH_REFRESH_TOKEN_TABLE}} SET is_revoked = TRUE WHERE user_id = $1',
87
+ [userId]
88
+ );
89
+
90
+ return {
91
+ success: true,
92
+ statusCode: 200,
93
+ message: 'Password updated successfully. Please sign in with your new password.',
94
+ timestamp: new Date().toISOString()
95
+ };
96
+ } catch (error) {
97
+ logger.error({ error: error.message }, '[auth-reset-password] Unexpected error');
98
+ return {
99
+ success: false,
100
+ statusCode: 500,
101
+ message: 'An internal server error occurred.',
102
+ timestamp: new Date().toISOString()
103
+ };
104
+ }
105
+ }
106
+ };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Auth Middleware (prototype, tanpa RBAC)
5
+ *
6
+ * Hanya menangani JWT: sign access token, verify Bearer token, dan guard
7
+ * `requireAuth` untuk melindungi endpoint lain. Tidak ada role/permission.
8
+ */
9
+
10
+ const jwt = require('jsonwebtoken');
11
+
12
+ function getJwtSecret() {
13
+ return process.env.JWT_SECRET || 'CHANGE-THIS-TO-RANDOM-SECRET-IN-PRODUCTION';
14
+ }
15
+
16
+ function getAccessTokenExpiryMin() {
17
+ return parseInt(process.env.ACCESS_TOKEN_EXPIRY_MIN || '60', 10);
18
+ }
19
+
20
+ /**
21
+ * Decode + verifikasi JWT dari header `Authorization: Bearer <token>`.
22
+ * @throws {Error} err.statusCode = 401 bila tidak valid.
23
+ */
24
+ function verifyToken(req) {
25
+ const authHeader = req.headers['authorization'];
26
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
27
+ const err = new Error('Token tidak ditemukan');
28
+ err.statusCode = 401;
29
+ throw err;
30
+ }
31
+
32
+ const token = authHeader.substring(7);
33
+ try {
34
+ return jwt.verify(token, getJwtSecret());
35
+ } catch (jwtErr) {
36
+ const err = new Error('Token tidak valid atau sudah kedaluwarsa');
37
+ err.statusCode = 401;
38
+ throw err;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Express middleware: verifikasi JWT lalu set req.user.
44
+ */
45
+ function requireAuth(req, res, next) {
46
+ try {
47
+ req.user = verifyToken(req);
48
+ next();
49
+ } catch (err) {
50
+ return res.status(err.statusCode || 401).json({
51
+ success: false,
52
+ message: err.message
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Generate JWT access token. Payload ramping: hanya identitas user.
59
+ */
60
+ function generateAccessToken(user) {
61
+ const payload = {
62
+ sub: user.user_id,
63
+ username: user.username,
64
+ email: user.email
65
+ };
66
+
67
+ return jwt.sign(payload, getJwtSecret(), {
68
+ algorithm: process.env.JWT_ALGORITHM || 'HS256',
69
+ expiresIn: `${getAccessTokenExpiryMin()}m`
70
+ });
71
+ }
72
+
73
+ module.exports = {
74
+ verifyToken,
75
+ requireAuth,
76
+ generateAccessToken,
77
+ getJwtSecret,
78
+ getAccessTokenExpiryMin
79
+ };