@restforgejs/platform 5.2.12 → 5.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/drift-check-linux +0 -0
- package/bin/sdf-tools-linux +0 -0
- package/bin/sdf-tools.exe +0 -0
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/fast-track.js +8 -9
- package/generators/cli/project/auth.js +209 -0
- package/generators/lib/auth/component-generator.js +58 -0
- package/generators/lib/auth/dependency-checker.js +102 -0
- package/generators/lib/auth/env-injector.js +81 -0
- package/generators/lib/auth/migrate-runner.js +111 -0
- package/generators/lib/auth/prefix.js +22 -0
- package/generators/lib/auth/processor-generator.js +55 -0
- package/generators/lib/auth/sdf-generator.js +102 -0
- package/generators/lib/auth/template-renderer.js +29 -0
- package/generators/lib/auth/templates/processor/login.js.tmpl +152 -0
- package/generators/lib/auth/templates/processor/logout.js.tmpl +58 -0
- package/generators/lib/auth/templates/processor/me.js.tmpl +64 -0
- package/generators/lib/auth/templates/processor/refresh.js.tmpl +134 -0
- package/generators/lib/auth/templates/processor/register.js.tmpl +77 -0
- package/generators/lib/auth/templates/processor/reset-password.js.tmpl +106 -0
- package/generators/lib/auth/templates/rfx_auth-middleware.js.tmpl +79 -0
- package/generators/lib/auth/templates/rfx_auth.js.tmpl +104 -0
- package/generators/lib/dbschema-kit/schema-printer.js +10 -1
- 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/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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generator processor auth (Fungsi 3b). Merender keenam 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` TIDAK disertakan (keputusan #5 campaign).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const { renderTemplate } = require('./template-renderer');
|
|
17
|
+
const FileUtils = require('../utils/file-utils');
|
|
18
|
+
const { AUTH_USER_TABLE, AUTH_REFRESH_TOKEN_TABLE, AUTH_MIDDLEWARE_NAME } = require('./prefix');
|
|
19
|
+
|
|
20
|
+
const TEMPLATES_DIR = path.join(__dirname, 'templates', 'processor');
|
|
21
|
+
|
|
22
|
+
const PROCESSOR_NAMES = ['register', 'login', 'refresh', 'logout', 'me', 'reset-password'];
|
|
23
|
+
|
|
24
|
+
const PARAMS = {
|
|
25
|
+
AUTH_USER_TABLE,
|
|
26
|
+
AUTH_REFRESH_TOKEN_TABLE,
|
|
27
|
+
AUTH_MIDDLEWARE_NAME
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {{ processorDir: string, force?: boolean }} options
|
|
32
|
+
* @returns {{ written: string[], skipped: string[] }}
|
|
33
|
+
*/
|
|
34
|
+
function generateAuthProcessors({ processorDir, force = false }) {
|
|
35
|
+
const written = [];
|
|
36
|
+
const skipped = [];
|
|
37
|
+
|
|
38
|
+
for (const name of PROCESSOR_NAMES) {
|
|
39
|
+
const targetPath = path.join(processorDir, `${name}.js`);
|
|
40
|
+
|
|
41
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
42
|
+
skipped.push(targetPath);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const templatePath = path.join(TEMPLATES_DIR, `${name}.js.tmpl`);
|
|
47
|
+
const content = renderTemplate(templatePath, PARAMS);
|
|
48
|
+
FileUtils.writeFileWithBackup(targetPath, content, true);
|
|
49
|
+
written.push(targetPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { written, skipped };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
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,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: 'Field username dan password wajib diisi.',
|
|
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: 'Username atau password salah.',
|
|
53
|
+
timestamp: new Date().toISOString()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!user.is_active) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
statusCode: 403,
|
|
61
|
+
message: 'Akun tidak aktif. Hubungi administrator.',
|
|
62
|
+
timestamp: new Date().toISOString()
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (user.is_locked) {
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
statusCode: 403,
|
|
70
|
+
message: 'Akun terkunci karena terlalu banyak percobaan login gagal. Hubungi 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
|
+
? 'Akun terkunci karena terlalu banyak percobaan login gagal. Hubungi administrator.'
|
|
90
|
+
: `Username atau password salah. Sisa percobaan: ${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 berhasil.',
|
|
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: 'Terjadi kesalahan internal pada server.',
|
|
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 berhasil.',
|
|
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: 'Terjadi kesalahan internal pada server.',
|
|
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 tidak ditemukan.',
|
|
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: 'Terjadi kesalahan internal pada server.',
|
|
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: 'Field refresh_token wajib diisi.',
|
|
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: 'Refresh token tidak valid.',
|
|
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: 'Refresh token tidak valid atau sudah kedaluwarsa.',
|
|
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 tidak aktif atau terkunci.',
|
|
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 berhasil diperbarui.',
|
|
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: 'Terjadi kesalahan internal pada server.',
|
|
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: 'Field username dan password wajib diisi.',
|
|
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 sudah terpakai.',
|
|
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: 'Registrasi berhasil.',
|
|
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: 'Terjadi kesalahan internal pada server.',
|
|
73
|
+
timestamp: new Date().toISOString()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|