@restforgejs/platform 5.2.13 → 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 +4 -2
- 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,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Contract: project auth
|
|
5
|
+
*
|
|
6
|
+
* Memasang "auth extension" (SDF, tabel, component, processor) ke project
|
|
7
|
+
* RESTForge yang sudah ada. Setelah validasi prasyarat (phase 00), handler
|
|
8
|
+
* menulis dua file SDF auth ber-prefix `rfx` ke `--schema-path` (Fungsi 1),
|
|
9
|
+
* lalu membuat tabelnya di DB via primitif dbschema-kit langsung (Fungsi 2),
|
|
10
|
+
* lalu menulis component middleware + router auth tanpa google (Fungsi 3a),
|
|
11
|
+
* lalu menulis keenam processor auth tanpa google (Fungsi 3b), lalu
|
|
12
|
+
* menginjeksi variabel env auth ke `--config` dan memverifikasi/mencatat
|
|
13
|
+
* dependency runtime `bcrypt`+`jsonwebtoken` (Fungsi tambahan, phase 05),
|
|
14
|
+
* lalu mencetak ringkasan akhir.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const { validateSafeName } = require('../../lib/utils/path-validator');
|
|
20
|
+
const projectRegistry = require('../../lib/utils/project-registry');
|
|
21
|
+
const { PREFIX } = require('../../lib/auth/prefix');
|
|
22
|
+
const { generateAuthSdf } = require('../../lib/auth/sdf-generator');
|
|
23
|
+
const { runAuthMigrate } = require('../../lib/auth/migrate-runner');
|
|
24
|
+
const { generateAuthComponents } = require('../../lib/auth/component-generator');
|
|
25
|
+
const { generateAuthProcessors } = require('../../lib/auth/processor-generator');
|
|
26
|
+
const { injectAuthEnv } = require('../../lib/auth/env-injector');
|
|
27
|
+
const { ensureAuthDependencies } = require('../../lib/auth/dependency-checker');
|
|
28
|
+
|
|
29
|
+
function projectExists(workingDir, projectName) {
|
|
30
|
+
const modulePath = path.join(workingDir, 'src', 'modules', `${projectName}.js`);
|
|
31
|
+
if (fs.existsSync(modulePath)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const registry = projectRegistry.loadProjectRegistry();
|
|
36
|
+
return Boolean(registry.projects && registry.projects[projectName]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
resource: 'project',
|
|
41
|
+
verb: 'auth',
|
|
42
|
+
description: 'Install auth extension (SDF, tabel, component, processor) ke project existing',
|
|
43
|
+
category: 'generation',
|
|
44
|
+
flags: {
|
|
45
|
+
create: {
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
required: false,
|
|
48
|
+
default: false,
|
|
49
|
+
description: 'Trigger eksekusi instalasi auth extension (wajib disertakan)'
|
|
50
|
+
},
|
|
51
|
+
project: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
required: false,
|
|
54
|
+
default: null,
|
|
55
|
+
description: 'Nama project target (kanonik; alias: --name)'
|
|
56
|
+
},
|
|
57
|
+
name: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
required: false,
|
|
60
|
+
default: null,
|
|
61
|
+
description: 'Alias dari --project'
|
|
62
|
+
},
|
|
63
|
+
'schema-path': {
|
|
64
|
+
type: 'string',
|
|
65
|
+
required: false,
|
|
66
|
+
default: './schema',
|
|
67
|
+
description: 'Folder output file SDF auth'
|
|
68
|
+
},
|
|
69
|
+
config: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
required: false,
|
|
72
|
+
default: 'config/db-connection.env',
|
|
73
|
+
description: 'File konfigurasi koneksi DB untuk langkah migrate'
|
|
74
|
+
},
|
|
75
|
+
force: {
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
required: false,
|
|
78
|
+
default: false,
|
|
79
|
+
description: 'Timpa file yang sudah ada (backup tetap dibuat)'
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
examples: [
|
|
83
|
+
'npx restforge project auth --create --project=myapp',
|
|
84
|
+
'npx restforge project auth --create --name=myapp --schema-path=./schema'
|
|
85
|
+
],
|
|
86
|
+
async handler(args) {
|
|
87
|
+
if (args.create !== true) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
'Flag --create wajib disertakan untuk memicu instalasi auth extension. ' +
|
|
90
|
+
'Contoh: npx restforge project auth --create --project=<nama-project>'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const rawName = args.project || args.name;
|
|
95
|
+
if (!rawName) {
|
|
96
|
+
throw new Error('Salah satu dari --project atau --name wajib diisi dengan nama project target');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const projectName = validateSafeName(rawName, 'project');
|
|
100
|
+
const workingDir = process.cwd();
|
|
101
|
+
|
|
102
|
+
if (!projectExists(workingDir, projectName)) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Project "${projectName}" tidak ditemukan (src/modules/${projectName}.js tidak ada). ` +
|
|
105
|
+
'Buat project lebih dulu (mis. "npx restforge endpoint create") sebelum menjalankan project auth.'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const schemaPath = path.resolve(workingDir, args['schema-path'] || './schema');
|
|
110
|
+
const { written, skipped } = generateAuthSdf({ schemaPath, force: args.force === true });
|
|
111
|
+
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(`Prasyarat OK untuk project "${projectName}" (prefix artefak: ${PREFIX}).`);
|
|
114
|
+
if (written.length > 0) {
|
|
115
|
+
console.log(`SDF auth ditulis: ${written.map((p) => path.basename(p)).join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
if (skipped.length > 0) {
|
|
118
|
+
console.log(
|
|
119
|
+
`SDF auth sudah ada, dilewati (gunakan --force untuk overwrite): ${skipped.map((p) => path.basename(p)).join(', ')}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const configPath = args.config || 'config/db-connection.env';
|
|
124
|
+
const migrateResult = await runAuthMigrate({ schemaPath, configPath });
|
|
125
|
+
console.log(
|
|
126
|
+
`Tabel auth siap: ${migrateResult.tables.join(', ')} ` +
|
|
127
|
+
`(dialect: ${migrateResult.dialect}, ${migrateResult.statementsApplied} statement diterapkan).`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const middlewareDir = path.join(workingDir, 'src', 'components', 'handlers');
|
|
131
|
+
const routerDir = path.join(workingDir, 'src', 'modules', projectName);
|
|
132
|
+
const { written: componentsWritten, skipped: componentsSkipped } = generateAuthComponents({
|
|
133
|
+
middlewareDir,
|
|
134
|
+
routerDir,
|
|
135
|
+
projectName,
|
|
136
|
+
force: args.force === true
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (componentsWritten.length > 0) {
|
|
140
|
+
console.log(
|
|
141
|
+
`Component/router auth ditulis: ${componentsWritten.map((p) => path.relative(workingDir, p)).join(', ')}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (componentsSkipped.length > 0) {
|
|
145
|
+
console.log(
|
|
146
|
+
`Component/router auth sudah ada, dilewati (gunakan --force untuk overwrite): ` +
|
|
147
|
+
`${componentsSkipped.map((p) => path.relative(workingDir, p)).join(', ')}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const processorDir = path.join(workingDir, 'src', 'modules', projectName, 'processor', 'auth');
|
|
152
|
+
const { written: processorsWritten, skipped: processorsSkipped } = generateAuthProcessors({
|
|
153
|
+
processorDir,
|
|
154
|
+
force: args.force === true
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (processorsWritten.length > 0) {
|
|
158
|
+
console.log(
|
|
159
|
+
`Processor auth ditulis: ${processorsWritten.map((p) => path.relative(workingDir, p)).join(', ')}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (processorsSkipped.length > 0) {
|
|
163
|
+
console.log(
|
|
164
|
+
`Processor auth sudah ada, dilewati (gunakan --force untuk overwrite): ` +
|
|
165
|
+
`${processorsSkipped.map((p) => path.relative(workingDir, p)).join(', ')}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const envResult = injectAuthEnv({ configPath });
|
|
169
|
+
const dependencyResult = ensureAuthDependencies({ workingDir });
|
|
170
|
+
|
|
171
|
+
console.log('');
|
|
172
|
+
console.log(`Environment auth (${path.relative(workingDir, envResult.filePath) || envResult.filePath}):`);
|
|
173
|
+
if (envResult.added.length > 0) {
|
|
174
|
+
console.log(` ditambahkan: ${envResult.added.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
if (envResult.skipped.length > 0) {
|
|
177
|
+
console.log(` sudah ada, dilewati (nilai existing dipertahankan): ${envResult.skipped.join(', ')}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log('Dependency runtime (bcrypt, jsonwebtoken):');
|
|
181
|
+
for (const [depName, info] of Object.entries(dependencyResult.resolution)) {
|
|
182
|
+
console.log(` ${depName}: ${info.resolvable ? `resolvable (${info.resolvedPath})` : 'TIDAK resolvable dari project target'}`);
|
|
183
|
+
}
|
|
184
|
+
if (dependencyResult.packageJson.hasPackageJson) {
|
|
185
|
+
if (dependencyResult.packageJson.added.length > 0) {
|
|
186
|
+
console.log(` package.json diperbarui, dependencies ditambahkan: ${dependencyResult.packageJson.added.join(', ')}`);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(' package.json project sudah mencantumkan bcrypt & jsonwebtoken (tidak diubah)');
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
console.log(' package.json tidak ditemukan di project target, tidak dicatat otomatis');
|
|
192
|
+
}
|
|
193
|
+
if (dependencyResult.needsInstallInstruction) {
|
|
194
|
+
console.log(' Jalankan "npm install bcrypt jsonwebtoken" di project target sebelum start server.');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log('Auth extension terpasang.');
|
|
199
|
+
console.log('Langkah lanjutan:');
|
|
200
|
+
if (envResult.jwtSecretGenerated) {
|
|
201
|
+
console.log(' - JWT_SECRET baru di-generate acak; rotate berkala sesuai kebijakan keamanan bila perlu.');
|
|
202
|
+
}
|
|
203
|
+
if (dependencyResult.needsInstallInstruction) {
|
|
204
|
+
console.log(' - Jalankan npm install agar bcrypt/jsonwebtoken terpasang sebelum start server.');
|
|
205
|
+
}
|
|
206
|
+
console.log(' - Restart server agar perubahan environment dan route auth termuat.');
|
|
207
|
+
console.log('');
|
|
208
|
+
}
|
|
209
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generator component middleware + router auth (Fungsi 3a). Merender aset
|
|
5
|
+
* template di `templates/` via template-renderer, lalu menulis ke lokasi
|
|
6
|
+
* target memakai writeFileWithBackup — perilaku force/skip identik Fungsi 1
|
|
7
|
+
* (sdf-generator.js): tanpa --force file existing di-skip, dengan --force
|
|
8
|
+
* di-overwrite + backup.
|
|
9
|
+
*
|
|
10
|
+
* Processor (Fungsi 3b, Phase 04) TIDAK ditulis di sini.
|
|
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_MIDDLEWARE_NAME, AUTH_ROUTER_NAME } = require('./prefix');
|
|
19
|
+
|
|
20
|
+
const TEMPLATES_DIR = path.join(__dirname, 'templates');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {{ middlewareDir: string, routerDir: string, projectName: string, force?: boolean }} options
|
|
24
|
+
* @returns {{ written: string[], skipped: string[] }}
|
|
25
|
+
*/
|
|
26
|
+
function generateAuthComponents({ middlewareDir, routerDir, projectName, force = false }) {
|
|
27
|
+
const targets = [
|
|
28
|
+
{
|
|
29
|
+
templateFile: 'rfx_auth-middleware.js.tmpl',
|
|
30
|
+
targetPath: path.join(middlewareDir, `${AUTH_MIDDLEWARE_NAME}.js`),
|
|
31
|
+
params: {}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
templateFile: 'rfx_auth.js.tmpl',
|
|
35
|
+
targetPath: path.join(routerDir, `${AUTH_ROUTER_NAME}.js`),
|
|
36
|
+
params: { PROJECT_NAME: projectName }
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const written = [];
|
|
41
|
+
const skipped = [];
|
|
42
|
+
|
|
43
|
+
for (const target of targets) {
|
|
44
|
+
if (fs.existsSync(target.targetPath) && !force) {
|
|
45
|
+
skipped.push(target.targetPath);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const templatePath = path.join(TEMPLATES_DIR, target.templateFile);
|
|
50
|
+
const content = renderTemplate(templatePath, target.params);
|
|
51
|
+
FileUtils.writeFileWithBackup(target.targetPath, content, true);
|
|
52
|
+
written.push(target.targetPath);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { written, skipped };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { generateAuthComponents };
|
|
@@ -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,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 };
|