@friggframework/core 2.0.0--canary.548.c8ae0ca.0 → 2.0.0--canary.545.c40eca4.0
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/CHANGELOG.md +66 -67
- package/CLAUDE.md +148 -124
- package/README.md +165 -130
- package/admin-scripts/index.js +52 -0
- package/admin-scripts/repositories/admin-api-key-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-api-key-repository-factory.js +51 -0
- package/admin-scripts/repositories/admin-api-key-repository-interface.js +121 -0
- package/admin-scripts/repositories/admin-api-key-repository-mongo.js +155 -0
- package/admin-scripts/repositories/admin-api-key-repository-postgres.js +189 -0
- package/admin-scripts/repositories/admin-process-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/admin-process-repository-factory.js +49 -0
- package/admin-scripts/repositories/admin-process-repository-interface.js +150 -0
- package/admin-scripts/repositories/admin-process-repository-mongo.js +213 -0
- package/admin-scripts/repositories/admin-process-repository-postgres.js +251 -0
- package/admin-scripts/repositories/script-execution-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-execution-repository-factory.js +51 -0
- package/admin-scripts/repositories/script-execution-repository-interface.js +193 -0
- package/admin-scripts/repositories/script-execution-repository-mongo.js +278 -0
- package/admin-scripts/repositories/script-execution-repository-postgres.js +320 -0
- package/admin-scripts/repositories/script-schedule-repository-documentdb.js +21 -0
- package/admin-scripts/repositories/script-schedule-repository-factory.js +49 -0
- package/admin-scripts/repositories/script-schedule-repository-interface.js +108 -0
- package/admin-scripts/repositories/script-schedule-repository-mongo.js +179 -0
- package/admin-scripts/repositories/script-schedule-repository-postgres.js +210 -0
- package/application/commands/admin-script-commands.js +348 -0
- package/application/commands/integration-commands.js +12 -6
- package/application/index.js +5 -3
- package/assertions/get.js +17 -7
- package/associations/association.js +59 -57
- package/core/CLAUDE.md +222 -156
- package/core/Worker.js +33 -12
- package/credential/credential-router.js +262 -0
- package/credential/repositories/credential-repository-documentdb.js +71 -1
- package/credential/repositories/credential-repository-factory.js +4 -6
- package/credential/repositories/credential-repository-interface.js +8 -2
- package/credential/repositories/credential-repository-mongo.js +45 -4
- package/credential/repositories/credential-repository-postgres.js +44 -3
- package/credential/repositories/credential-repository.js +3 -1
- package/credential/use-cases/delete-credential-for-user.js +50 -0
- package/credential/use-cases/get-credential-for-user.js +21 -3
- package/credential/use-cases/list-credentials-for-user.js +31 -0
- package/credential/use-cases/reauthorize-credential.js +103 -0
- package/credential/use-cases/update-authentication-status.js +5 -2
- package/database/MONGODB_TRANSACTION_FIX.md +72 -52
- package/database/config.js +13 -59
- package/database/documentdb-encryption-service.js +29 -13
- package/database/documentdb-utils.js +21 -12
- package/database/encryption/README.md +53 -36
- package/database/encryption/encryption-schema-registry.js +29 -23
- package/database/encryption/field-encryption-service.js +15 -4
- package/database/encryption/logger.js +3 -2
- package/database/encryption/prisma-encryption-extension.js +5 -8
- package/database/prisma.js +22 -14
- package/database/repositories/health-check-repository-factory.js +7 -5
- package/database/repositories/health-check-repository-interface.js +15 -5
- package/database/repositories/health-check-repository-postgres.js +1 -1
- package/database/use-cases/check-database-health-use-case.js +3 -2
- package/database/use-cases/check-database-state-use-case.js +8 -7
- package/database/use-cases/get-database-state-via-worker-use-case.js +15 -12
- package/database/use-cases/get-migration-status-use-case.js +4 -2
- package/database/use-cases/run-database-migration-use-case.js +41 -9
- package/database/use-cases/test-encryption-use-case.js +4 -5
- package/database/use-cases/trigger-database-migration-use-case.js +9 -8
- package/database/utils/mongodb-collection-utils.js +11 -3
- package/database/utils/mongodb-schema-init.js +10 -4
- package/database/utils/prisma-runner.js +144 -74
- package/database/utils/prisma-schema-parser.js +2 -2
- package/docs/PROCESS_MANAGEMENT_QUEUE_SPEC.md +135 -80
- package/encrypt/Cryptor.js +54 -49
- package/encrypt/aes-encryption-key-provider.js +82 -0
- package/encrypt/encryption-key-provider-interface.js +50 -0
- package/errors/fetch-error.js +1 -1
- package/generated/prisma-mongodb/edge.js +59 -4
- package/generated/prisma-mongodb/index-browser.js +49 -1
- package/generated/prisma-mongodb/index.d.ts +4023 -61
- package/generated/prisma-mongodb/index.js +59 -4
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +87 -0
- package/generated/prisma-mongodb/wasm.js +59 -4
- package/generated/prisma-postgresql/edge.js +58 -4
- package/generated/prisma-postgresql/index-browser.js +48 -1
- package/generated/prisma-postgresql/index.d.ts +4738 -54
- package/generated/prisma-postgresql/index.js +58 -4
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +85 -0
- package/generated/prisma-postgresql/wasm.js +58 -4
- package/handlers/WEBHOOKS.md +104 -81
- package/handlers/app-definition-loader.js +53 -10
- package/handlers/app-handler-helpers.js +14 -3
- package/handlers/backend-utils.js +0 -1
- package/handlers/database-migration-handler.js +64 -45
- package/handlers/middleware/admin-auth.js +53 -0
- package/handlers/routers/HEALTHCHECK.md +170 -135
- package/handlers/routers/admin.js +474 -0
- package/handlers/routers/auth.js +51 -9
- package/handlers/routers/db-migration.handler.js +2 -3
- package/handlers/routers/db-migration.js +102 -66
- package/handlers/routers/docs.js +180 -0
- package/handlers/routers/health.js +84 -283
- package/handlers/routers/integration-defined-routers.js +44 -33
- package/handlers/routers/integration-webhook-routers.js +73 -52
- package/handlers/routers/middleware/loadUser.js +39 -0
- package/handlers/routers/middleware/requireAdmin.js +43 -0
- package/handlers/routers/middleware/requireLoggedInUser.js +19 -0
- package/handlers/routers/user.js +30 -14
- package/handlers/routers/websocket.js +18 -4
- package/handlers/use-cases/check-integrations-health-use-case.js +13 -6
- package/handlers/workers/db-migration.js +74 -35
- package/handlers/workers/integration-defined-workers.js +31 -20
- package/index.js +50 -16
- package/infrastructure/scheduler/index.js +15 -11
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +185 -0
- package/infrastructure/scheduler/scheduler-service-factory.js +38 -31
- package/integrations/WEBHOOK-QUICKSTART.md +21 -14
- package/integrations/index.js +4 -0
- package/integrations/integration-base.js +9 -6
- package/integrations/integration-router.js +1828 -157
- package/integrations/options.js +47 -20
- package/integrations/repositories/integration-mapping-repository-factory.js +4 -8
- package/integrations/repositories/integration-repository-documentdb.js +53 -24
- package/integrations/repositories/integration-repository-factory.js +6 -6
- package/integrations/repositories/integration-repository-interface.js +27 -9
- package/integrations/repositories/integration-repository-postgres.js +3 -3
- package/integrations/repositories/process-repository-factory.js +4 -7
- package/integrations/repositories/process-repository-interface.js +9 -5
- package/integrations/repositories/process-repository-mongo.js +7 -3
- package/integrations/repositories/process-repository-postgres.js +2 -2
- package/integrations/tests/doubles/config-capturing-integration.js +9 -8
- package/integrations/tests/doubles/dummy-integration-class.js +81 -8
- package/integrations/tests/doubles/test-integration-repository.js +51 -11
- package/integrations/use-cases/create-integration.js +36 -32
- package/integrations/use-cases/create-process.js +20 -9
- package/integrations/use-cases/delete-integration-for-user.js +5 -4
- package/integrations/use-cases/execute-proxy-request.js +564 -0
- package/integrations/use-cases/get-integration-for-user.js +25 -10
- package/integrations/use-cases/get-integration-instance-by-definition.js +13 -7
- package/integrations/use-cases/get-integrations-for-user.js +4 -1
- package/integrations/use-cases/get-possible-integrations.js +1 -1
- package/integrations/use-cases/get-process.js +5 -6
- package/integrations/use-cases/index.js +3 -1
- package/integrations/use-cases/load-integration-context.js +1 -5
- package/integrations/use-cases/update-process-state.js +10 -8
- package/integrations/utils/map-integration-dto.js +9 -3
- package/jest-setup.js +1 -1
- package/jest-teardown.js +1 -1
- package/lambda/TimeoutCatcher.js +32 -30
- package/lambda/index.js +1 -1
- package/logs/index.js +3 -3
- package/modules/domain/entities/AuthorizationSession.js +131 -0
- package/modules/domain/entities/index.js +10 -0
- package/modules/module.js +4 -9
- package/modules/repositories/authorization-session-repository-factory.js +53 -0
- package/modules/repositories/authorization-session-repository-interface.js +75 -0
- package/modules/repositories/authorization-session-repository-mongo.js +169 -0
- package/modules/repositories/authorization-session-repository-postgres.js +170 -0
- package/modules/repositories/module-repository-documentdb.js +89 -41
- package/modules/repositories/module-repository-factory.js +4 -6
- package/modules/repositories/module-repository-interface.js +13 -0
- package/modules/repositories/module-repository-mongo.js +40 -5
- package/modules/repositories/module-repository-postgres.js +42 -5
- package/modules/repositories/module-repository.js +28 -0
- package/modules/requester/api-key.js +0 -2
- package/modules/requester/basic.js +0 -1
- package/modules/requester/oauth-2.js +3 -14
- package/modules/test/mock-api/definition.js +15 -10
- package/modules/test/mock-api/mocks/hubspot.js +36 -36
- package/modules/tests/doubles/test-module-factory.js +10 -4
- package/modules/tests/doubles/test-module-repository.js +1 -1
- package/modules/use-cases/delete-module-entity.js +23 -0
- package/modules/use-cases/get-authorization-requirements.js +110 -0
- package/modules/use-cases/get-entities-for-user.js +1 -1
- package/modules/use-cases/get-entity-options-by-id.js +8 -6
- package/modules/use-cases/get-module-entity-by-id.js +16 -0
- package/modules/use-cases/get-module.js +12 -9
- package/modules/use-cases/process-authorization-callback.js +12 -6
- package/modules/use-cases/process-authorization-step.js +160 -0
- package/modules/use-cases/refresh-entity-options.js +8 -6
- package/modules/use-cases/start-authorization-session.js +86 -0
- package/modules/use-cases/test-module-auth.js +8 -6
- package/modules/use-cases/update-module-entity.js +27 -0
- package/modules/utils/map-module-dto.js +2 -2
- package/openapi/openapi-spec-generator.js +370 -0
- package/openapi/openapi-v1.yaml +1360 -0
- package/openapi/openapi-v2.yaml +1535 -0
- package/package.json +7 -9
- package/prisma-mongodb/schema.prisma +87 -0
- package/prisma-postgresql/migrations/20260222000000_add_admin_process_and_script_schedule/migration.sql +55 -0
- package/prisma-postgresql/schema.prisma +85 -0
- package/providers/index.js +13 -0
- package/providers/resolve-provider.js +93 -0
- package/queues/index.js +19 -0
- package/queues/providers/index.js +7 -0
- package/queues/providers/netlify-background-provider.js +76 -0
- package/queues/providers/qstash-queue-provider.js +122 -0
- package/queues/queue-client-interface.js +55 -0
- package/queues/queue-provider-factory.js +93 -0
- package/queues/queue-provider.js +46 -0
- package/queues/queuer-util.js +33 -25
- package/syncs/repositories/sync-repository-documentdb.js +39 -19
- package/syncs/repositories/sync-repository-factory.js +4 -4
- package/syncs/repositories/sync-repository-interface.js +6 -2
- package/syncs/repositories/sync-repository-postgres.js +19 -13
- package/syncs/sync.js +104 -100
- package/token/repositories/token-repository-factory.js +4 -6
- package/types/assertions/index.d.ts +72 -72
- package/types/core/index.d.ts +98 -53
- package/types/errors/index.d.ts +61 -54
- package/types/eslint-config/index.d.ts +40 -40
- package/types/lambda/index.d.ts +26 -26
- package/types/logs/index.d.ts +4 -4
- package/types/prettier-config/index.d.ts +5 -5
- package/types/test-environment/index.d.ts +14 -14
- package/types/tsconfig.json +95 -93
- package/user/repositories/user-repository-factory.js +4 -4
- package/user/tests/doubles/test-user-repository.js +5 -2
- package/user/use-cases/authenticate-user.js +2 -4
- package/user/use-cases/authenticate-with-shared-secret.js +1 -1
- package/user/use-cases/create-individual-user.js +8 -7
- package/user/use-cases/create-organization-user.js +1 -1
- package/user/use-cases/create-token-for-user-id.js +1 -1
- package/user/use-cases/delete-user.js +47 -0
- package/user/use-cases/get-user-from-adopter-jwt.js +0 -2
- package/user/use-cases/get-user-from-bearer-token.js +7 -3
- package/user/use-cases/get-user-from-x-frigg-headers.js +14 -11
- package/user/use-cases/login-user.js +8 -8
- package/user/user.js +18 -4
- package/utils/backend-path.js +2 -2
- package/utils/index.js +4 -1
- package/websocket/repositories/websocket-connection-repository-documentdb.js +46 -21
- package/websocket/repositories/websocket-connection-repository-factory.js +4 -8
- package/websocket/repositories/websocket-connection-repository-mongo.js +26 -16
- package/websocket/repositories/websocket-connection-repository-postgres.js +26 -15
- package/websocket/repositories/websocket-connection-repository.js +26 -20
- package/websocket/websocket-message-sender-interface.js +38 -0
- package/database/adapters/lambda-invoker.js +0 -97
- package/database/repositories/migration-status-repository-s3.js +0 -137
- package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +0 -184
|
@@ -17,6 +17,15 @@ const {
|
|
|
17
17
|
const {
|
|
18
18
|
GetCredentialForUser,
|
|
19
19
|
} = require('../credential/use-cases/get-credential-for-user');
|
|
20
|
+
const {
|
|
21
|
+
ListCredentialsForUser,
|
|
22
|
+
} = require('../credential/use-cases/list-credentials-for-user');
|
|
23
|
+
const {
|
|
24
|
+
DeleteCredentialForUser,
|
|
25
|
+
} = require('../credential/use-cases/delete-credential-for-user');
|
|
26
|
+
const {
|
|
27
|
+
ReauthorizeCredential,
|
|
28
|
+
} = require('../credential/use-cases/reauthorize-credential');
|
|
20
29
|
const { CreateIntegration } = require('./use-cases/create-integration');
|
|
21
30
|
const { ModuleFactory } = require('../modules/module-factory');
|
|
22
31
|
const {
|
|
@@ -69,6 +78,19 @@ const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
|
|
|
69
78
|
const {
|
|
70
79
|
ProcessAuthorizationCallback,
|
|
71
80
|
} = require('../modules/use-cases/process-authorization-callback');
|
|
81
|
+
const {
|
|
82
|
+
createAuthorizationSessionRepository,
|
|
83
|
+
} = require('../modules/repositories/authorization-session-repository-factory');
|
|
84
|
+
const {
|
|
85
|
+
StartAuthorizationSessionUseCase,
|
|
86
|
+
} = require('../modules/use-cases/start-authorization-session');
|
|
87
|
+
const {
|
|
88
|
+
ProcessAuthorizationStepUseCase,
|
|
89
|
+
} = require('../modules/use-cases/process-authorization-step');
|
|
90
|
+
const {
|
|
91
|
+
GetAuthorizationRequirementsUseCase,
|
|
92
|
+
} = require('../modules/use-cases/get-authorization-requirements');
|
|
93
|
+
const { ExecuteProxyRequest } = require('./use-cases/execute-proxy-request');
|
|
72
94
|
|
|
73
95
|
function createIntegrationRouter() {
|
|
74
96
|
const { integrations: integrationClasses, userConfig } =
|
|
@@ -77,6 +99,7 @@ function createIntegrationRouter() {
|
|
|
77
99
|
const integrationRepository = createIntegrationRepository();
|
|
78
100
|
const credentialRepository = createCredentialRepository();
|
|
79
101
|
const userRepository = createUserRepository();
|
|
102
|
+
const authSessionRepository = createAuthorizationSessionRepository();
|
|
80
103
|
|
|
81
104
|
const getUserFromBearerToken = new GetUserFromBearerToken({
|
|
82
105
|
userRepository,
|
|
@@ -103,10 +126,18 @@ function createIntegrationRouter() {
|
|
|
103
126
|
userConfig,
|
|
104
127
|
});
|
|
105
128
|
|
|
129
|
+
// Support both integration classes and direct module definitions (for testing)
|
|
130
|
+
const isModuleDefinitionFormat =
|
|
131
|
+
integrationClasses &&
|
|
132
|
+
integrationClasses[0] &&
|
|
133
|
+
integrationClasses[0].moduleName &&
|
|
134
|
+
integrationClasses[0].definition;
|
|
135
|
+
|
|
106
136
|
const moduleFactory = new ModuleFactory({
|
|
107
137
|
moduleRepository,
|
|
108
|
-
moduleDefinitions:
|
|
109
|
-
|
|
138
|
+
moduleDefinitions: isModuleDefinitionFormat
|
|
139
|
+
? integrationClasses
|
|
140
|
+
: getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
110
141
|
});
|
|
111
142
|
const deleteIntegrationForUser = new DeleteIntegrationForUser({
|
|
112
143
|
integrationRepository,
|
|
@@ -125,16 +156,32 @@ function createIntegrationRouter() {
|
|
|
125
156
|
credentialRepository,
|
|
126
157
|
});
|
|
127
158
|
|
|
159
|
+
const listCredentialsForUser = new ListCredentialsForUser({
|
|
160
|
+
credentialRepository,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const deleteCredentialForUser = new DeleteCredentialForUser({
|
|
164
|
+
credentialRepository,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const reauthorizeCredential = new ReauthorizeCredential({
|
|
168
|
+
credentialRepository,
|
|
169
|
+
moduleRepository,
|
|
170
|
+
});
|
|
171
|
+
|
|
128
172
|
const createIntegration = new CreateIntegration({
|
|
129
173
|
integrationRepository,
|
|
130
174
|
integrationClasses,
|
|
131
175
|
moduleFactory,
|
|
132
176
|
});
|
|
133
177
|
|
|
178
|
+
const moduleDefinitions = isModuleDefinitionFormat
|
|
179
|
+
? integrationClasses
|
|
180
|
+
: getModulesDefinitionFromIntegrationClasses(integrationClasses);
|
|
181
|
+
|
|
134
182
|
const getEntitiesForUser = new GetEntitiesForUser({
|
|
135
183
|
moduleRepository,
|
|
136
|
-
moduleDefinitions
|
|
137
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
184
|
+
moduleDefinitions,
|
|
138
185
|
});
|
|
139
186
|
|
|
140
187
|
const getIntegrationInstance = new GetIntegrationInstance({
|
|
@@ -150,37 +197,31 @@ function createIntegrationRouter() {
|
|
|
150
197
|
});
|
|
151
198
|
|
|
152
199
|
const getModuleInstanceFromType = new GetModuleInstanceFromType({
|
|
153
|
-
moduleDefinitions
|
|
154
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
200
|
+
moduleDefinitions,
|
|
155
201
|
});
|
|
156
202
|
|
|
157
203
|
const getEntityOptionsByType = new GetEntityOptionsByType({
|
|
158
|
-
moduleDefinitions
|
|
159
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
204
|
+
moduleDefinitions,
|
|
160
205
|
});
|
|
161
206
|
|
|
162
207
|
const testModuleAuth = new TestModuleAuth({
|
|
163
208
|
moduleRepository,
|
|
164
|
-
moduleDefinitions
|
|
165
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
209
|
+
moduleDefinitions,
|
|
166
210
|
});
|
|
167
211
|
|
|
168
212
|
const getModule = new GetModule({
|
|
169
213
|
moduleRepository,
|
|
170
|
-
moduleDefinitions
|
|
171
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
214
|
+
moduleDefinitions,
|
|
172
215
|
});
|
|
173
216
|
|
|
174
217
|
const getEntityOptionsById = new GetEntityOptionsById({
|
|
175
218
|
moduleRepository,
|
|
176
|
-
moduleDefinitions
|
|
177
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
219
|
+
moduleDefinitions,
|
|
178
220
|
});
|
|
179
221
|
|
|
180
222
|
const refreshEntityOptions = new RefreshEntityOptions({
|
|
181
223
|
moduleRepository,
|
|
182
|
-
moduleDefinitions
|
|
183
|
-
getModulesDefinitionFromIntegrationClasses(integrationClasses),
|
|
224
|
+
moduleDefinitions,
|
|
184
225
|
});
|
|
185
226
|
|
|
186
227
|
const getPossibleIntegrations = new GetPossibleIntegrations({
|
|
@@ -190,8 +231,28 @@ function createIntegrationRouter() {
|
|
|
190
231
|
const processAuthorizationCallback = new ProcessAuthorizationCallback({
|
|
191
232
|
moduleRepository,
|
|
192
233
|
credentialRepository,
|
|
193
|
-
moduleDefinitions
|
|
194
|
-
|
|
234
|
+
moduleDefinitions,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const startAuthorizationSession = new StartAuthorizationSessionUseCase({
|
|
238
|
+
authSessionRepository,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const processAuthorizationStep = new ProcessAuthorizationStepUseCase({
|
|
242
|
+
authSessionRepository,
|
|
243
|
+
moduleDefinitions,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const getAuthorizationRequirements =
|
|
247
|
+
new GetAuthorizationRequirementsUseCase({
|
|
248
|
+
moduleDefinitions,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const executeProxyRequest = new ExecuteProxyRequest({
|
|
252
|
+
moduleRepository,
|
|
253
|
+
credentialRepository,
|
|
254
|
+
moduleFactory,
|
|
255
|
+
moduleDefinitions,
|
|
195
256
|
});
|
|
196
257
|
|
|
197
258
|
const router = express();
|
|
@@ -214,6 +275,22 @@ function createIntegrationRouter() {
|
|
|
214
275
|
getEntityOptionsById,
|
|
215
276
|
refreshEntityOptions,
|
|
216
277
|
processAuthorizationCallback,
|
|
278
|
+
moduleDefinitions,
|
|
279
|
+
startAuthorizationSession,
|
|
280
|
+
processAuthorizationStep,
|
|
281
|
+
getAuthorizationRequirements,
|
|
282
|
+
moduleRepository,
|
|
283
|
+
credentialRepository,
|
|
284
|
+
authSessionRepository,
|
|
285
|
+
executeProxyRequest,
|
|
286
|
+
});
|
|
287
|
+
setCredentialRoutes(router, authenticateUser, {
|
|
288
|
+
listCredentialsForUser,
|
|
289
|
+
getCredentialForUser,
|
|
290
|
+
deleteCredentialForUser,
|
|
291
|
+
reauthorizeCredential,
|
|
292
|
+
getAuthorizationRequirements,
|
|
293
|
+
moduleDefinitions,
|
|
217
294
|
});
|
|
218
295
|
return router;
|
|
219
296
|
}
|
|
@@ -258,20 +335,84 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
258
335
|
updateIntegration,
|
|
259
336
|
getPossibleIntegrations,
|
|
260
337
|
} = useCases;
|
|
338
|
+
|
|
339
|
+
// =========================================================================
|
|
340
|
+
// v1 API Routes (backwards compatible - legacy format)
|
|
341
|
+
// =========================================================================
|
|
342
|
+
|
|
343
|
+
// GET /api/integrations - v1 format: combined response with entities and integrations
|
|
261
344
|
router.route('/api/integrations').get(
|
|
262
345
|
catchAsyncError(async (req, res) => {
|
|
263
346
|
const user = await authenticateUser.execute(req);
|
|
264
347
|
const userId = user.getId();
|
|
265
|
-
|
|
266
|
-
|
|
348
|
+
|
|
349
|
+
// v1 returns everything in one call
|
|
350
|
+
const [integrations, options, authorized] = await Promise.all([
|
|
351
|
+
getIntegrationsForUser.execute(userId),
|
|
352
|
+
getPossibleIntegrations.execute(),
|
|
353
|
+
getEntitiesForUser.execute(userId),
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
res.json({
|
|
267
357
|
entities: {
|
|
268
|
-
options
|
|
269
|
-
authorized
|
|
358
|
+
options,
|
|
359
|
+
authorized,
|
|
270
360
|
},
|
|
271
|
-
integrations
|
|
272
|
-
};
|
|
361
|
+
integrations,
|
|
362
|
+
});
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// GET /api/integrations/options - Get available integration options (v1 compatible)
|
|
367
|
+
router.route('/api/integrations/options').get(
|
|
368
|
+
catchAsyncError(async (req, res) => {
|
|
369
|
+
const options = await getPossibleIntegrations.execute();
|
|
370
|
+
res.json({ integrations: options });
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// GET /api/entities - Get user's connected entities/accounts (v1 compatible)
|
|
375
|
+
router.route('/api/entities').get(
|
|
376
|
+
catchAsyncError(async (req, res) => {
|
|
377
|
+
const user = await authenticateUser.execute(req);
|
|
378
|
+
const userId = user.getId();
|
|
379
|
+
const entities = await getEntitiesForUser.execute(userId);
|
|
380
|
+
|
|
381
|
+
res.json({ entities });
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// =========================================================================
|
|
386
|
+
// v2 API Routes (new clean format)
|
|
387
|
+
// =========================================================================
|
|
388
|
+
|
|
389
|
+
// GET /api/v2/integrations - v2 format: only integrations
|
|
390
|
+
router.route('/api/v2/integrations').get(
|
|
391
|
+
catchAsyncError(async (req, res) => {
|
|
392
|
+
const user = await authenticateUser.execute(req);
|
|
393
|
+
const userId = user.getId();
|
|
394
|
+
const integrations = await getIntegrationsForUser.execute(userId);
|
|
395
|
+
|
|
396
|
+
res.json({ integrations });
|
|
397
|
+
})
|
|
398
|
+
);
|
|
273
399
|
|
|
274
|
-
|
|
400
|
+
// GET /api/v2/integrations/options - Get available integration options
|
|
401
|
+
router.route('/api/v2/integrations/options').get(
|
|
402
|
+
catchAsyncError(async (req, res) => {
|
|
403
|
+
const options = await getPossibleIntegrations.execute();
|
|
404
|
+
res.json({ integrations: options });
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// GET /api/v2/entities - Get user's connected entities/accounts
|
|
409
|
+
router.route('/api/v2/entities').get(
|
|
410
|
+
catchAsyncError(async (req, res) => {
|
|
411
|
+
const user = await authenticateUser.execute(req);
|
|
412
|
+
const userId = user.getId();
|
|
413
|
+
const entities = await getEntitiesForUser.execute(userId);
|
|
414
|
+
|
|
415
|
+
res.json({ entities });
|
|
275
416
|
})
|
|
276
417
|
);
|
|
277
418
|
|
|
@@ -480,187 +621,1717 @@ function setIntegrationRoutes(router, authenticateUser, useCases) {
|
|
|
480
621
|
}
|
|
481
622
|
})
|
|
482
623
|
);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Sets up entity-related routes for the integration router
|
|
487
|
-
* @param {Object} router - Express router instance
|
|
488
|
-
* @param {import('../user/use-cases/authenticate-user').AuthenticateUser} authenticateUser - Use case for multi-mode user authentication
|
|
489
|
-
*/
|
|
490
|
-
function setEntityRoutes(router, authenticateUser, useCases) {
|
|
491
|
-
const {
|
|
492
|
-
getCredentialForUser,
|
|
493
|
-
getModuleInstanceFromType,
|
|
494
|
-
getEntityOptionsByType,
|
|
495
|
-
testModuleAuth,
|
|
496
|
-
getModule,
|
|
497
|
-
getEntityOptionsById,
|
|
498
|
-
refreshEntityOptions,
|
|
499
|
-
processAuthorizationCallback,
|
|
500
|
-
} = useCases;
|
|
501
|
-
|
|
502
|
-
router.route('/api/authorize').get(
|
|
503
|
-
catchAsyncError(async (req, res) => {
|
|
504
|
-
const user = await authenticateUser.execute(req);
|
|
505
|
-
const userId = user.getId();
|
|
506
|
-
const params = checkRequiredParams(req.query, ['entityType']);
|
|
507
|
-
const module = await getModuleInstanceFromType.execute(
|
|
508
|
-
userId,
|
|
509
|
-
params.entityType
|
|
510
|
-
);
|
|
511
|
-
const areRequirementsValid =
|
|
512
|
-
module.validateAuthorizationRequirements();
|
|
513
|
-
if (!areRequirementsValid) {
|
|
514
|
-
throw new Error(
|
|
515
|
-
`Error: Entity of type ${params.entityType} requires a valid url`
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
624
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
625
|
+
// =========================================================================
|
|
626
|
+
// v2 API Routes - Integration endpoints (can evolve independently from v1)
|
|
627
|
+
// =========================================================================
|
|
522
628
|
|
|
523
|
-
|
|
629
|
+
// POST /api/v2/integrations - Create integration (v2)
|
|
630
|
+
router.route('/api/v2/integrations').post(
|
|
524
631
|
catchAsyncError(async (req, res) => {
|
|
525
632
|
const user = await authenticateUser.execute(req);
|
|
526
633
|
const userId = user.getId();
|
|
527
634
|
const params = checkRequiredParams(req.body, [
|
|
528
|
-
'
|
|
529
|
-
'
|
|
635
|
+
'entities',
|
|
636
|
+
'config',
|
|
530
637
|
]);
|
|
531
638
|
|
|
532
|
-
|
|
639
|
+
get(params.config, 'type');
|
|
640
|
+
|
|
641
|
+
const integration = await createIntegration.execute(
|
|
642
|
+
params.entities,
|
|
533
643
|
userId,
|
|
534
|
-
params.
|
|
535
|
-
params.data
|
|
644
|
+
params.config
|
|
536
645
|
);
|
|
537
646
|
|
|
538
|
-
res.json(
|
|
647
|
+
res.status(201).json(integration);
|
|
539
648
|
})
|
|
540
649
|
);
|
|
541
650
|
|
|
542
|
-
|
|
651
|
+
// PATCH /api/v2/integrations/:integrationId - Update integration (v2)
|
|
652
|
+
router.route('/api/v2/integrations/:integrationId').patch(
|
|
543
653
|
catchAsyncError(async (req, res) => {
|
|
544
654
|
const user = await authenticateUser.execute(req);
|
|
545
655
|
const userId = user.getId();
|
|
546
|
-
const params = checkRequiredParams(req.body, [
|
|
547
|
-
'entityType',
|
|
548
|
-
'data',
|
|
549
|
-
]);
|
|
550
|
-
checkRequiredParams(req.body.data, ['credential_id']);
|
|
551
|
-
|
|
552
|
-
// May want to pass along the user ID as well so credential ID's can't be fished???
|
|
553
|
-
const credential = await getCredentialForUser.execute(
|
|
554
|
-
params.data.credential_id,
|
|
555
|
-
userId
|
|
556
|
-
);
|
|
557
|
-
|
|
558
|
-
if (!credential) {
|
|
559
|
-
throw Boom.badRequest('Invalid credential ID');
|
|
560
|
-
}
|
|
656
|
+
const params = checkRequiredParams(req.body, ['config']);
|
|
561
657
|
|
|
562
|
-
const
|
|
658
|
+
const integration = await updateIntegration.execute(
|
|
659
|
+
req.params.integrationId,
|
|
563
660
|
userId,
|
|
564
|
-
params.
|
|
565
|
-
);
|
|
566
|
-
const entityDetails = await module.getEntityDetails(
|
|
567
|
-
module.api,
|
|
568
|
-
null,
|
|
569
|
-
null,
|
|
570
|
-
userId
|
|
661
|
+
params.config
|
|
571
662
|
);
|
|
572
|
-
|
|
573
|
-
res.json(await module.findOrCreateEntity(entityDetails));
|
|
663
|
+
res.json(integration);
|
|
574
664
|
})
|
|
575
665
|
);
|
|
576
666
|
|
|
577
|
-
|
|
667
|
+
// DELETE /api/v2/integrations/:integrationId - Delete integration (v2)
|
|
668
|
+
router.route('/api/v2/integrations/:integrationId').delete(
|
|
578
669
|
catchAsyncError(async (req, res) => {
|
|
579
670
|
const user = await authenticateUser.execute(req);
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
req.params.credentialId,
|
|
585
|
-
userId
|
|
586
|
-
);
|
|
587
|
-
if (credential.userId.toString() !== userId) {
|
|
588
|
-
throw Boom.forbidden('Credential does not belong to user');
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const params = checkRequiredParams(req.query, ['entityType']);
|
|
592
|
-
const entityOptions = await getEntityOptionsByType.execute(
|
|
593
|
-
userId,
|
|
594
|
-
params.entityType
|
|
671
|
+
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
672
|
+
await deleteIntegrationForUser.execute(
|
|
673
|
+
params.integrationId,
|
|
674
|
+
user.getId()
|
|
595
675
|
);
|
|
596
|
-
|
|
597
|
-
res.json(entityOptions);
|
|
676
|
+
res.status(204).json({});
|
|
598
677
|
})
|
|
599
678
|
);
|
|
600
679
|
|
|
601
|
-
|
|
680
|
+
// GET /api/v2/integrations/:integrationId - Get single integration (v2)
|
|
681
|
+
router.route('/api/v2/integrations/:integrationId').get(
|
|
602
682
|
catchAsyncError(async (req, res) => {
|
|
603
683
|
const user = await authenticateUser.execute(req);
|
|
604
|
-
const params = checkRequiredParams(req.params, ['entityId']);
|
|
605
|
-
const testAuthResponse = await testModuleAuth.execute(
|
|
606
|
-
params.entityId,
|
|
607
|
-
user // Pass User object for proper validation
|
|
608
|
-
);
|
|
609
684
|
|
|
610
|
-
if (!
|
|
611
|
-
|
|
612
|
-
res.json({
|
|
613
|
-
errors: [
|
|
614
|
-
{
|
|
615
|
-
title: 'Authentication Error',
|
|
616
|
-
message: `There was an error with your Entity. Please reconnect/re-authenticate, or reach out to Support for assistance.`,
|
|
617
|
-
timestamp: Date.now(),
|
|
618
|
-
},
|
|
619
|
-
],
|
|
620
|
-
});
|
|
621
|
-
} else {
|
|
622
|
-
res.json({ status: 'ok' });
|
|
685
|
+
if (!user) {
|
|
686
|
+
throw Boom.forbidden('User not found');
|
|
623
687
|
}
|
|
624
|
-
})
|
|
625
|
-
);
|
|
626
688
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
689
|
+
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
690
|
+
const integration = await getIntegrationInstance.execute(
|
|
691
|
+
params.integrationId,
|
|
692
|
+
user.getId()
|
|
693
|
+
);
|
|
632
694
|
|
|
633
|
-
res.json(
|
|
695
|
+
res.json({
|
|
696
|
+
id: integration.id,
|
|
697
|
+
entities: integration.entities,
|
|
698
|
+
status: integration.status,
|
|
699
|
+
config: integration.config,
|
|
700
|
+
});
|
|
634
701
|
})
|
|
635
702
|
);
|
|
636
703
|
|
|
637
|
-
|
|
704
|
+
// GET /api/v2/integrations/:integrationId/config/options - Get config options (v2)
|
|
705
|
+
router.route('/api/v2/integrations/:integrationId/config/options').get(
|
|
638
706
|
catchAsyncError(async (req, res) => {
|
|
639
707
|
const user = await authenticateUser.execute(req);
|
|
640
|
-
const params = checkRequiredParams(req.params, ['
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
user // Pass User object
|
|
708
|
+
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
709
|
+
const integration = await getIntegrationInstance.execute(
|
|
710
|
+
params.integrationId,
|
|
711
|
+
user.getId()
|
|
645
712
|
);
|
|
646
|
-
|
|
647
|
-
res.json(entityOptions);
|
|
713
|
+
res.json(await integration.send('GET_CONFIG_OPTIONS'));
|
|
648
714
|
})
|
|
649
715
|
);
|
|
650
716
|
|
|
651
|
-
|
|
717
|
+
// POST /api/v2/integrations/:integrationId/config/options/refresh (v2)
|
|
718
|
+
router
|
|
719
|
+
.route('/api/v2/integrations/:integrationId/config/options/refresh')
|
|
720
|
+
.post(
|
|
721
|
+
catchAsyncError(async (req, res) => {
|
|
722
|
+
const user = await authenticateUser.execute(req);
|
|
723
|
+
const params = checkRequiredParams(req.params, [
|
|
724
|
+
'integrationId',
|
|
725
|
+
]);
|
|
726
|
+
const integration = await getIntegrationInstance.execute(
|
|
727
|
+
params.integrationId,
|
|
728
|
+
user.getId()
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
res.json(
|
|
732
|
+
await integration.send('REFRESH_CONFIG_OPTIONS', req.body)
|
|
733
|
+
);
|
|
734
|
+
})
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
// ALL /api/v2/integrations/:integrationId/actions - Get user actions (v2)
|
|
738
|
+
router.route('/api/v2/integrations/:integrationId/actions').all(
|
|
652
739
|
catchAsyncError(async (req, res) => {
|
|
653
740
|
const user = await authenticateUser.execute(req);
|
|
654
|
-
const params = checkRequiredParams(req.params, ['
|
|
655
|
-
const
|
|
656
|
-
params.
|
|
657
|
-
user
|
|
658
|
-
req.body
|
|
741
|
+
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
742
|
+
const integration = await getIntegrationInstance.execute(
|
|
743
|
+
params.integrationId,
|
|
744
|
+
user.getId()
|
|
659
745
|
);
|
|
660
|
-
|
|
661
|
-
res.json(updatedOptions);
|
|
746
|
+
res.json(await integration.send('GET_USER_ACTIONS', req.body));
|
|
662
747
|
})
|
|
663
748
|
);
|
|
749
|
+
|
|
750
|
+
// ALL /api/v2/integrations/:integrationId/actions/:actionId/options (v2)
|
|
751
|
+
router
|
|
752
|
+
.route('/api/v2/integrations/:integrationId/actions/:actionId/options')
|
|
753
|
+
.all(
|
|
754
|
+
catchAsyncError(async (req, res) => {
|
|
755
|
+
const user = await authenticateUser.execute(req);
|
|
756
|
+
const params = checkRequiredParams(req.params, [
|
|
757
|
+
'integrationId',
|
|
758
|
+
'actionId',
|
|
759
|
+
]);
|
|
760
|
+
const integration = await getIntegrationInstance.execute(
|
|
761
|
+
params.integrationId,
|
|
762
|
+
user.getId()
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
res.json(
|
|
766
|
+
await integration.send('GET_USER_ACTION_OPTIONS', {
|
|
767
|
+
actionId: params.actionId,
|
|
768
|
+
data: req.body,
|
|
769
|
+
})
|
|
770
|
+
);
|
|
771
|
+
})
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
// POST /api/v2/integrations/:integrationId/actions/:actionId/options/refresh (v2)
|
|
775
|
+
router
|
|
776
|
+
.route(
|
|
777
|
+
'/api/v2/integrations/:integrationId/actions/:actionId/options/refresh'
|
|
778
|
+
)
|
|
779
|
+
.post(
|
|
780
|
+
catchAsyncError(async (req, res) => {
|
|
781
|
+
const user = await authenticateUser.execute(req);
|
|
782
|
+
const params = checkRequiredParams(req.params, [
|
|
783
|
+
'integrationId',
|
|
784
|
+
'actionId',
|
|
785
|
+
]);
|
|
786
|
+
const integration = await getIntegrationInstance.execute(
|
|
787
|
+
params.integrationId,
|
|
788
|
+
user.getId()
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
res.json(
|
|
792
|
+
await integration.send('REFRESH_USER_ACTION_OPTIONS', {
|
|
793
|
+
actionId: params.actionId,
|
|
794
|
+
data: req.body,
|
|
795
|
+
})
|
|
796
|
+
);
|
|
797
|
+
})
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
// POST /api/v2/integrations/:integrationId/actions/:actionId - Execute action (v2)
|
|
801
|
+
router.route('/api/v2/integrations/:integrationId/actions/:actionId').post(
|
|
802
|
+
catchAsyncError(async (req, res) => {
|
|
803
|
+
const user = await authenticateUser.execute(req);
|
|
804
|
+
const params = checkRequiredParams(req.params, [
|
|
805
|
+
'integrationId',
|
|
806
|
+
'actionId',
|
|
807
|
+
]);
|
|
808
|
+
const integration = await getIntegrationInstance.execute(
|
|
809
|
+
params.integrationId,
|
|
810
|
+
user.getId()
|
|
811
|
+
);
|
|
812
|
+
res.json(await integration.send(params.actionId, req.body));
|
|
813
|
+
})
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// GET /api/v2/integrations/:integrationId/test-auth - Test auth (v2)
|
|
817
|
+
router.route('/api/v2/integrations/:integrationId/test-auth').get(
|
|
818
|
+
catchAsyncError(async (req, res) => {
|
|
819
|
+
const user = await authenticateUser.execute(req);
|
|
820
|
+
const params = checkRequiredParams(req.params, ['integrationId']);
|
|
821
|
+
const instance = await getIntegrationInstance.execute(
|
|
822
|
+
params.integrationId,
|
|
823
|
+
user.getId()
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
if (!instance) {
|
|
827
|
+
throw Boom.notFound();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const start = Date.now();
|
|
831
|
+
await instance.testAuth();
|
|
832
|
+
const errors = instance.record.messages?.errors?.filter(
|
|
833
|
+
({ timestamp }) => timestamp >= start
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
if (errors?.length) {
|
|
837
|
+
res.status(400);
|
|
838
|
+
res.json({ errors });
|
|
839
|
+
} else {
|
|
840
|
+
res.json({ status: 'ok' });
|
|
841
|
+
}
|
|
842
|
+
})
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Sets up entity-related routes for the integration router
|
|
848
|
+
* @param {Object} router - Express router instance
|
|
849
|
+
* @param {import('../user/use-cases/authenticate-user').AuthenticateUser} authenticateUser - Use case for multi-mode user authentication
|
|
850
|
+
*/
|
|
851
|
+
function setEntityRoutes(router, authenticateUser, useCases) {
|
|
852
|
+
const {
|
|
853
|
+
getCredentialForUser,
|
|
854
|
+
getModuleInstanceFromType,
|
|
855
|
+
getEntityOptionsByType,
|
|
856
|
+
testModuleAuth,
|
|
857
|
+
getModule,
|
|
858
|
+
getEntityOptionsById,
|
|
859
|
+
refreshEntityOptions,
|
|
860
|
+
processAuthorizationCallback,
|
|
861
|
+
moduleDefinitions,
|
|
862
|
+
startAuthorizationSession,
|
|
863
|
+
processAuthorizationStep,
|
|
864
|
+
getAuthorizationRequirements,
|
|
865
|
+
moduleRepository,
|
|
866
|
+
credentialRepository,
|
|
867
|
+
authSessionRepository,
|
|
868
|
+
executeProxyRequest,
|
|
869
|
+
} = useCases;
|
|
870
|
+
|
|
871
|
+
// GET /api/authorize - Get authorization requirements (supports multi-step)
|
|
872
|
+
router.route('/api/authorize').get(
|
|
873
|
+
catchAsyncError(async (req, res) => {
|
|
874
|
+
const user = await authenticateUser.execute(req);
|
|
875
|
+
const userId = user.getId();
|
|
876
|
+
const params = checkRequiredParams(req.query, ['entityType']);
|
|
877
|
+
const step = parseInt(req.query.step || '1', 10);
|
|
878
|
+
const sessionId = req.query.sessionId;
|
|
879
|
+
|
|
880
|
+
// Validate session if step > 1
|
|
881
|
+
if (step > 1 && !sessionId) {
|
|
882
|
+
throw Boom.badRequest('sessionId required for step > 1');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Check if module supports multi-step auth
|
|
886
|
+
const requirements = await getAuthorizationRequirements.execute(
|
|
887
|
+
params.entityType,
|
|
888
|
+
step
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
// Generate session ID for multi-step flows on step 1
|
|
892
|
+
if (requirements.isMultiStep && step === 1) {
|
|
893
|
+
const crypto = require('crypto');
|
|
894
|
+
requirements.sessionId = crypto.randomUUID();
|
|
895
|
+
} else if (sessionId) {
|
|
896
|
+
requirements.sessionId = sessionId;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Validate requirements for backward compatibility
|
|
900
|
+
if (!requirements.isMultiStep) {
|
|
901
|
+
const module = await getModuleInstanceFromType.execute(
|
|
902
|
+
userId,
|
|
903
|
+
params.entityType
|
|
904
|
+
);
|
|
905
|
+
const areRequirementsValid =
|
|
906
|
+
module.validateAuthorizationRequirements();
|
|
907
|
+
if (!areRequirementsValid) {
|
|
908
|
+
throw new Error(
|
|
909
|
+
`Error: Entity of type ${params.entityType} requires a valid url`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
res.json(requirements);
|
|
915
|
+
})
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
// POST /api/authorize - Process authorization (supports multi-step)
|
|
919
|
+
router.route('/api/authorize').post(
|
|
920
|
+
catchAsyncError(async (req, res) => {
|
|
921
|
+
const user = await authenticateUser.execute(req);
|
|
922
|
+
const userId = user.getId();
|
|
923
|
+
const params = checkRequiredParams(req.body, [
|
|
924
|
+
'entityType',
|
|
925
|
+
'data',
|
|
926
|
+
]);
|
|
927
|
+
const step = parseInt(req.body.step || '1', 10);
|
|
928
|
+
const sessionId = req.body.sessionId;
|
|
929
|
+
const isGlobal = req.body.isGlobal || false;
|
|
930
|
+
|
|
931
|
+
// Find module definition to check step count
|
|
932
|
+
const moduleDefinition = moduleDefinitions.find(
|
|
933
|
+
(def) => def.moduleName === params.entityType
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
if (!moduleDefinition) {
|
|
937
|
+
throw Boom.badRequest(
|
|
938
|
+
`Unknown entity type: ${params.entityType}`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const ModuleDefinition = moduleDefinition.definition;
|
|
943
|
+
const stepCount = ModuleDefinition.getAuthStepCount
|
|
944
|
+
? ModuleDefinition.getAuthStepCount()
|
|
945
|
+
: 1;
|
|
946
|
+
|
|
947
|
+
// Single-step flow - use existing ProcessAuthorizationCallback
|
|
948
|
+
if (stepCount === 1) {
|
|
949
|
+
const entityDetails =
|
|
950
|
+
await processAuthorizationCallback.execute(
|
|
951
|
+
userId,
|
|
952
|
+
params.entityType,
|
|
953
|
+
params.data,
|
|
954
|
+
isGlobal
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
return res.json(entityDetails);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Multi-step flow
|
|
961
|
+
if (!sessionId) {
|
|
962
|
+
throw Boom.badRequest(
|
|
963
|
+
'sessionId required for multi-step authorization'
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
let session;
|
|
968
|
+
|
|
969
|
+
if (step === 1) {
|
|
970
|
+
// Create new session for step 1
|
|
971
|
+
session = await startAuthorizationSession.execute(
|
|
972
|
+
userId,
|
|
973
|
+
params.entityType,
|
|
974
|
+
stepCount
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
// Override with client-provided sessionId
|
|
978
|
+
session.sessionId = sessionId;
|
|
979
|
+
await useCases.authSessionRepository?.update(session);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Process this step
|
|
983
|
+
const result = await processAuthorizationStep.execute(
|
|
984
|
+
sessionId,
|
|
985
|
+
userId,
|
|
986
|
+
step,
|
|
987
|
+
params.data
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
if (result.completed) {
|
|
991
|
+
// Final step - create entity using standard flow
|
|
992
|
+
const entityDetails =
|
|
993
|
+
await processAuthorizationCallback.execute(
|
|
994
|
+
userId,
|
|
995
|
+
params.entityType,
|
|
996
|
+
result.authData,
|
|
997
|
+
isGlobal
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
return res.json(entityDetails);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Return next step requirements
|
|
1004
|
+
res.json({
|
|
1005
|
+
step: result.nextStep,
|
|
1006
|
+
totalSteps: result.totalSteps,
|
|
1007
|
+
sessionId: result.sessionId,
|
|
1008
|
+
requirements: result.requirements,
|
|
1009
|
+
message: result.message,
|
|
1010
|
+
});
|
|
1011
|
+
})
|
|
1012
|
+
);
|
|
1013
|
+
|
|
1014
|
+
router.route('/api/entities').post(
|
|
1015
|
+
catchAsyncError(async (req, res) => {
|
|
1016
|
+
const user = await authenticateUser.execute(req);
|
|
1017
|
+
const userId = user.getId();
|
|
1018
|
+
const params = checkRequiredParams(req.body, [
|
|
1019
|
+
'entityType',
|
|
1020
|
+
'data',
|
|
1021
|
+
]);
|
|
1022
|
+
checkRequiredParams(req.body.data, ['credential_id']);
|
|
1023
|
+
|
|
1024
|
+
// May want to pass along the user ID as well so credential ID's can't be fished???
|
|
1025
|
+
const credential = await getCredentialForUser.execute(
|
|
1026
|
+
params.data.credential_id,
|
|
1027
|
+
userId
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
if (!credential) {
|
|
1031
|
+
throw Boom.badRequest('Invalid credential ID');
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const module = await getModuleInstanceFromType.execute(
|
|
1035
|
+
userId,
|
|
1036
|
+
params.entityType
|
|
1037
|
+
);
|
|
1038
|
+
const entityDetails = await module.getEntityDetails(
|
|
1039
|
+
module.api,
|
|
1040
|
+
null,
|
|
1041
|
+
null,
|
|
1042
|
+
userId
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
res.json(await module.findOrCreateEntity(entityDetails));
|
|
1046
|
+
})
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
// GET /api/entities/types - List all available entity types
|
|
1050
|
+
// NOTE: This route MUST come before /api/entities/:entityId
|
|
1051
|
+
router.route('/api/entities/types').get(
|
|
1052
|
+
catchAsyncError(async (req, res) => {
|
|
1053
|
+
await authenticateUser.execute(req);
|
|
1054
|
+
|
|
1055
|
+
// Map module definitions to entity type format
|
|
1056
|
+
const types = moduleDefinitions.map((moduleDef) => {
|
|
1057
|
+
const Definition = moduleDef.definition;
|
|
1058
|
+
|
|
1059
|
+
return {
|
|
1060
|
+
type: moduleDef.moduleName,
|
|
1061
|
+
name:
|
|
1062
|
+
typeof Definition.getDisplayName === 'function'
|
|
1063
|
+
? Definition.getDisplayName()
|
|
1064
|
+
: moduleDef.moduleName,
|
|
1065
|
+
description:
|
|
1066
|
+
typeof Definition.getDescription === 'function'
|
|
1067
|
+
? Definition.getDescription()
|
|
1068
|
+
: undefined,
|
|
1069
|
+
authType:
|
|
1070
|
+
typeof Definition.getAuthType === 'function'
|
|
1071
|
+
? Definition.getAuthType()
|
|
1072
|
+
: 'oauth2',
|
|
1073
|
+
isMultiStep:
|
|
1074
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1075
|
+
? Definition.getAuthStepCount() > 1
|
|
1076
|
+
: false,
|
|
1077
|
+
stepCount:
|
|
1078
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1079
|
+
? Definition.getAuthStepCount()
|
|
1080
|
+
: 1,
|
|
1081
|
+
capabilities:
|
|
1082
|
+
typeof Definition.getCapabilities === 'function'
|
|
1083
|
+
? Definition.getCapabilities()
|
|
1084
|
+
: undefined,
|
|
1085
|
+
};
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// Sort by name
|
|
1089
|
+
types.sort((a, b) => a.name.localeCompare(b.name));
|
|
1090
|
+
|
|
1091
|
+
res.json({ types });
|
|
1092
|
+
})
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
// GET /api/entities/types/:typeName - Get details for specific entity type
|
|
1096
|
+
// NOTE: This route MUST come before /api/entities/:entityId
|
|
1097
|
+
router.route('/api/entities/types/:typeName').get(
|
|
1098
|
+
catchAsyncError(async (req, res) => {
|
|
1099
|
+
await authenticateUser.execute(req);
|
|
1100
|
+
const { typeName } = req.params;
|
|
1101
|
+
|
|
1102
|
+
// Validate type name format
|
|
1103
|
+
if (!/^[a-z0-9-_]+$/i.test(typeName)) {
|
|
1104
|
+
throw Boom.badRequest('Invalid type name format');
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Find module definition
|
|
1108
|
+
const moduleDef = moduleDefinitions.find(
|
|
1109
|
+
(def) => def.moduleName === typeName
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
if (!moduleDef) {
|
|
1113
|
+
throw Boom.notFound(`Entity type '${typeName}' not found`);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const Definition = moduleDef.definition;
|
|
1117
|
+
|
|
1118
|
+
const entityType = {
|
|
1119
|
+
type: moduleDef.moduleName,
|
|
1120
|
+
name:
|
|
1121
|
+
typeof Definition.getDisplayName === 'function'
|
|
1122
|
+
? Definition.getDisplayName()
|
|
1123
|
+
: moduleDef.moduleName,
|
|
1124
|
+
description:
|
|
1125
|
+
typeof Definition.getDescription === 'function'
|
|
1126
|
+
? Definition.getDescription()
|
|
1127
|
+
: undefined,
|
|
1128
|
+
authType:
|
|
1129
|
+
typeof Definition.getAuthType === 'function'
|
|
1130
|
+
? Definition.getAuthType()
|
|
1131
|
+
: 'oauth2',
|
|
1132
|
+
isMultiStep:
|
|
1133
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1134
|
+
? Definition.getAuthStepCount() > 1
|
|
1135
|
+
: false,
|
|
1136
|
+
stepCount:
|
|
1137
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1138
|
+
? Definition.getAuthStepCount()
|
|
1139
|
+
: 1,
|
|
1140
|
+
capabilities:
|
|
1141
|
+
typeof Definition.getCapabilities === 'function'
|
|
1142
|
+
? Definition.getCapabilities()
|
|
1143
|
+
: undefined,
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
res.json(entityType);
|
|
1147
|
+
})
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
// GET /api/entities/types/:typeName/requirements - Get auth requirements for entity type
|
|
1151
|
+
// NOTE: This route MUST come before /api/entities/:entityId
|
|
1152
|
+
router.route('/api/entities/types/:typeName/requirements').get(
|
|
1153
|
+
catchAsyncError(async (req, res) => {
|
|
1154
|
+
await authenticateUser.execute(req);
|
|
1155
|
+
const { typeName } = req.params;
|
|
1156
|
+
const step = parseInt(req.query.step || '1', 10);
|
|
1157
|
+
const sessionId = req.query.sessionId;
|
|
1158
|
+
|
|
1159
|
+
// Validate step
|
|
1160
|
+
if (step < 1) {
|
|
1161
|
+
throw Boom.badRequest('step must be >= 1');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Find module definition
|
|
1165
|
+
const moduleDef = moduleDefinitions.find(
|
|
1166
|
+
(def) => def.moduleName === typeName
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
if (!moduleDef) {
|
|
1170
|
+
throw Boom.notFound(`Entity type '${typeName}' not found`);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const Definition = moduleDef.definition;
|
|
1174
|
+
const stepCount =
|
|
1175
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1176
|
+
? Definition.getAuthStepCount()
|
|
1177
|
+
: 1;
|
|
1178
|
+
|
|
1179
|
+
// Validate step is within range
|
|
1180
|
+
if (step > stepCount) {
|
|
1181
|
+
throw Boom.badRequest(
|
|
1182
|
+
`step ${step} exceeds total steps (${stepCount})`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Validate sessionId for step > 1
|
|
1187
|
+
if (step > 1 && !sessionId) {
|
|
1188
|
+
throw Boom.badRequest('sessionId required for step > 1');
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// For multi-step, validate sessionId format
|
|
1192
|
+
if (step > 1 && sessionId) {
|
|
1193
|
+
// Basic validation - sessionId should be a non-empty string
|
|
1194
|
+
if (typeof sessionId !== 'string' || sessionId.trim() === '') {
|
|
1195
|
+
throw Boom.badRequest('Invalid sessionId format');
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Get requirements from module definition
|
|
1200
|
+
const requirements = await getAuthorizationRequirements.execute(
|
|
1201
|
+
typeName,
|
|
1202
|
+
step
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
// Add sessionId for multi-step flows
|
|
1206
|
+
if (stepCount > 1) {
|
|
1207
|
+
if (step === 1) {
|
|
1208
|
+
// Generate new sessionId for step 1
|
|
1209
|
+
const crypto = require('crypto');
|
|
1210
|
+
requirements.sessionId = crypto.randomUUID();
|
|
1211
|
+
} else {
|
|
1212
|
+
// Use provided sessionId for subsequent steps
|
|
1213
|
+
requirements.sessionId = sessionId;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
res.json(requirements);
|
|
1218
|
+
})
|
|
1219
|
+
);
|
|
1220
|
+
|
|
1221
|
+
// GET /api/entities/options/:credentialId - Get entity options for credential
|
|
1222
|
+
// NOTE: This route MUST come before /api/entities/:entityId
|
|
1223
|
+
router.route('/api/entities/options/:credentialId').get(
|
|
1224
|
+
catchAsyncError(async (req, res) => {
|
|
1225
|
+
const user = await authenticateUser.execute(req);
|
|
1226
|
+
const userId = user.getId();
|
|
1227
|
+
// TODO May want to pass along the user ID as well so credential ID's can't be fished???
|
|
1228
|
+
// TODO **flagging this for review** -MW
|
|
1229
|
+
const credential = await getCredentialForUser.execute(
|
|
1230
|
+
req.params.credentialId,
|
|
1231
|
+
userId
|
|
1232
|
+
);
|
|
1233
|
+
if (credential.userId.toString() !== userId) {
|
|
1234
|
+
throw Boom.forbidden('Credential does not belong to user');
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const params = checkRequiredParams(req.query, ['entityType']);
|
|
1238
|
+
const entityOptions = await getEntityOptionsByType.execute(
|
|
1239
|
+
userId,
|
|
1240
|
+
params.entityType
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
res.json(entityOptions);
|
|
1244
|
+
})
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
// GET /api/entities/:entityId/test-auth - Test authentication for entity
|
|
1248
|
+
router.route('/api/entities/:entityId/test-auth').get(
|
|
1249
|
+
catchAsyncError(async (req, res) => {
|
|
1250
|
+
const user = await authenticateUser.execute(req);
|
|
1251
|
+
const params = checkRequiredParams(req.params, ['entityId']);
|
|
1252
|
+
const testAuthResponse = await testModuleAuth.execute(
|
|
1253
|
+
params.entityId,
|
|
1254
|
+
user // Pass User object for proper validation
|
|
1255
|
+
);
|
|
1256
|
+
|
|
1257
|
+
if (!testAuthResponse) {
|
|
1258
|
+
res.status(400);
|
|
1259
|
+
res.json({
|
|
1260
|
+
errors: [
|
|
1261
|
+
{
|
|
1262
|
+
title: 'Authentication Error',
|
|
1263
|
+
message: `There was an error with your Entity. Please reconnect/re-authenticate, or reach out to Support for assistance.`,
|
|
1264
|
+
timestamp: Date.now(),
|
|
1265
|
+
},
|
|
1266
|
+
],
|
|
1267
|
+
});
|
|
1268
|
+
} else {
|
|
1269
|
+
res.json({ status: 'ok' });
|
|
1270
|
+
}
|
|
1271
|
+
})
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
// GET /api/entities/:entityId - Get entity by ID
|
|
1275
|
+
router.route('/api/entities/:entityId').get(
|
|
1276
|
+
catchAsyncError(async (req, res) => {
|
|
1277
|
+
const user = await authenticateUser.execute(req);
|
|
1278
|
+
const params = checkRequiredParams(req.params, ['entityId']);
|
|
1279
|
+
const module = await getModule.execute(params.entityId, user); // Pass User object
|
|
1280
|
+
|
|
1281
|
+
res.json(module);
|
|
1282
|
+
})
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
// POST /api/entities/:entityId/options - Get entity options by ID
|
|
1286
|
+
router.route('/api/entities/:entityId/options').post(
|
|
1287
|
+
catchAsyncError(async (req, res) => {
|
|
1288
|
+
const user = await authenticateUser.execute(req);
|
|
1289
|
+
const params = checkRequiredParams(req.params, ['entityId']);
|
|
1290
|
+
|
|
1291
|
+
const entityOptions = await getEntityOptionsById.execute(
|
|
1292
|
+
params.entityId,
|
|
1293
|
+
user // Pass User object
|
|
1294
|
+
);
|
|
1295
|
+
|
|
1296
|
+
res.json(entityOptions);
|
|
1297
|
+
})
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
// POST /api/entities/:entityId/options/refresh - Refresh entity options
|
|
1301
|
+
router.route('/api/entities/:entityId/options/refresh').post(
|
|
1302
|
+
catchAsyncError(async (req, res) => {
|
|
1303
|
+
const user = await authenticateUser.execute(req);
|
|
1304
|
+
const params = checkRequiredParams(req.params, ['entityId']);
|
|
1305
|
+
const updatedOptions = await refreshEntityOptions.execute(
|
|
1306
|
+
params.entityId,
|
|
1307
|
+
user, // Pass User object
|
|
1308
|
+
req.body
|
|
1309
|
+
);
|
|
1310
|
+
|
|
1311
|
+
res.json(updatedOptions);
|
|
1312
|
+
})
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
// POST /api/entities/:id/reauthorize - Reauthorize specific entity
|
|
1316
|
+
router.route('/api/entities/:id/reauthorize').post(
|
|
1317
|
+
catchAsyncError(async (req, res) => {
|
|
1318
|
+
const user = await authenticateUser.execute(req);
|
|
1319
|
+
const userId = user.getId();
|
|
1320
|
+
const entityId = req.params.id;
|
|
1321
|
+
|
|
1322
|
+
// Validate data parameter
|
|
1323
|
+
const params = checkRequiredParams(req.body, ['data']);
|
|
1324
|
+
if (typeof params.data !== 'object' || Array.isArray(params.data)) {
|
|
1325
|
+
throw Boom.badRequest('data must be an object');
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const step = parseInt(req.body.step || '1', 10);
|
|
1329
|
+
const sessionId = req.body.sessionId;
|
|
1330
|
+
|
|
1331
|
+
// Validate step
|
|
1332
|
+
if (step < 1) {
|
|
1333
|
+
throw Boom.badRequest('step must be >= 1');
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Get entity
|
|
1337
|
+
const entity = await moduleRepository?.findById(entityId);
|
|
1338
|
+
if (!entity) {
|
|
1339
|
+
throw Boom.notFound('Entity not found');
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Check ownership
|
|
1343
|
+
if (entity.userId.toString() !== userId) {
|
|
1344
|
+
throw Boom.forbidden(
|
|
1345
|
+
'User is not authorized to access this entity'
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Get credential
|
|
1350
|
+
const credential = await credentialRepository?.findById(
|
|
1351
|
+
entity.credentialId
|
|
1352
|
+
);
|
|
1353
|
+
|
|
1354
|
+
// Find module definition
|
|
1355
|
+
const moduleDef = moduleDefinitions.find(
|
|
1356
|
+
(def) => def.moduleName === entity.type
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1359
|
+
if (!moduleDef) {
|
|
1360
|
+
throw Boom.badRequest(`Unknown entity type: ${entity.type}`);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const Definition = moduleDef.definition;
|
|
1364
|
+
const stepCount =
|
|
1365
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1366
|
+
? Definition.getAuthStepCount()
|
|
1367
|
+
: 1;
|
|
1368
|
+
|
|
1369
|
+
// Validate step is within range
|
|
1370
|
+
if (step > stepCount) {
|
|
1371
|
+
throw Boom.badRequest(
|
|
1372
|
+
`step ${step} exceeds total steps (${stepCount})`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Single-step reauthorization
|
|
1377
|
+
if (stepCount === 1) {
|
|
1378
|
+
try {
|
|
1379
|
+
// Process reauthorization using existing flow
|
|
1380
|
+
const result = await processAuthorizationCallback.execute(
|
|
1381
|
+
userId,
|
|
1382
|
+
entity.type,
|
|
1383
|
+
params.data
|
|
1384
|
+
);
|
|
1385
|
+
|
|
1386
|
+
// Update entity status
|
|
1387
|
+
const updatedEntity = await moduleRepository?.update({
|
|
1388
|
+
id: entityId,
|
|
1389
|
+
authIsValid: true,
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
res.json({
|
|
1393
|
+
success: true,
|
|
1394
|
+
credential_id: result.credential_id,
|
|
1395
|
+
entity_id: entityId,
|
|
1396
|
+
authIsValid: true,
|
|
1397
|
+
});
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
throw Boom.badRequest(
|
|
1400
|
+
error.message || 'Reauthorization failed'
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
} else {
|
|
1404
|
+
// Multi-step reauthorization
|
|
1405
|
+
if (step > 1 && !sessionId) {
|
|
1406
|
+
throw Boom.badRequest('sessionId required for step > 1');
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
let session;
|
|
1410
|
+
|
|
1411
|
+
if (step === 1) {
|
|
1412
|
+
// Create new session for step 1
|
|
1413
|
+
session = await startAuthorizationSession.execute(
|
|
1414
|
+
userId,
|
|
1415
|
+
entity.type,
|
|
1416
|
+
stepCount
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
// Override with client-provided sessionId
|
|
1420
|
+
if (sessionId) {
|
|
1421
|
+
session.sessionId = sessionId;
|
|
1422
|
+
await authSessionRepository?.update(session);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Process this step
|
|
1427
|
+
const result = await processAuthorizationStep.execute(
|
|
1428
|
+
sessionId || session?.sessionId,
|
|
1429
|
+
userId,
|
|
1430
|
+
step,
|
|
1431
|
+
params.data
|
|
1432
|
+
);
|
|
1433
|
+
|
|
1434
|
+
if (result.completed) {
|
|
1435
|
+
// Final step - update credential and entity
|
|
1436
|
+
try {
|
|
1437
|
+
const authResult =
|
|
1438
|
+
await processAuthorizationCallback.execute(
|
|
1439
|
+
userId,
|
|
1440
|
+
entity.type,
|
|
1441
|
+
result.authData
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
// Update entity status
|
|
1445
|
+
await moduleRepository?.update({
|
|
1446
|
+
id: entityId,
|
|
1447
|
+
authIsValid: true,
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
return res.json({
|
|
1451
|
+
success: true,
|
|
1452
|
+
credential_id: authResult.credential_id,
|
|
1453
|
+
entity_id: entityId,
|
|
1454
|
+
authIsValid: true,
|
|
1455
|
+
});
|
|
1456
|
+
} catch (error) {
|
|
1457
|
+
throw Boom.badRequest(
|
|
1458
|
+
error.message || 'Reauthorization failed'
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// Return next step requirements
|
|
1464
|
+
res.json({
|
|
1465
|
+
step: result.nextStep,
|
|
1466
|
+
totalSteps: result.totalSteps,
|
|
1467
|
+
sessionId: result.sessionId,
|
|
1468
|
+
requirements: result.requirements,
|
|
1469
|
+
message: result.message,
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
})
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
// POST /api/entities/:id/proxy - Proxy request through entity's API connection
|
|
1476
|
+
router.route('/api/entities/:id/proxy').post(
|
|
1477
|
+
catchAsyncError(async (req, res) => {
|
|
1478
|
+
const user = await authenticateUser.execute(req);
|
|
1479
|
+
const userId = user.getId();
|
|
1480
|
+
const entityId = req.params.id;
|
|
1481
|
+
|
|
1482
|
+
try {
|
|
1483
|
+
// Execute proxy request via entity
|
|
1484
|
+
const proxyResponse =
|
|
1485
|
+
await executeProxyRequest.executeViaEntity(
|
|
1486
|
+
entityId,
|
|
1487
|
+
userId,
|
|
1488
|
+
req.body
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
// Return success response
|
|
1492
|
+
res.status(200).json(proxyResponse);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
// Handle Boom errors
|
|
1495
|
+
if (Boom.isBoom(error)) {
|
|
1496
|
+
const statusCode = error.output.statusCode;
|
|
1497
|
+
const errorData = error.data || {};
|
|
1498
|
+
|
|
1499
|
+
// Build error response matching proxyErrorResponse schema
|
|
1500
|
+
const errorResponse = {
|
|
1501
|
+
success: false,
|
|
1502
|
+
status: statusCode,
|
|
1503
|
+
error: {
|
|
1504
|
+
code:
|
|
1505
|
+
errorData.code ||
|
|
1506
|
+
_getErrorCodeFromStatus(statusCode),
|
|
1507
|
+
message:
|
|
1508
|
+
error.output.payload.message || error.message,
|
|
1509
|
+
details: errorData.details || null,
|
|
1510
|
+
},
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
// Add upstreamStatus if present
|
|
1514
|
+
if (errorData.upstreamStatus) {
|
|
1515
|
+
errorResponse.error.upstreamStatus =
|
|
1516
|
+
errorData.upstreamStatus;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
return res.status(statusCode).json(errorResponse);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Unknown error - return 500
|
|
1523
|
+
res.status(500).json({
|
|
1524
|
+
success: false,
|
|
1525
|
+
status: 500,
|
|
1526
|
+
error: {
|
|
1527
|
+
code: 'INTERNAL_ERROR',
|
|
1528
|
+
message: 'An unexpected error occurred',
|
|
1529
|
+
details: null,
|
|
1530
|
+
},
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
})
|
|
1534
|
+
);
|
|
1535
|
+
|
|
1536
|
+
// POST /api/credentials/:id/proxy - Proxy request through credential's API connection
|
|
1537
|
+
router.route('/api/credentials/:id/proxy').post(
|
|
1538
|
+
catchAsyncError(async (req, res) => {
|
|
1539
|
+
const user = await authenticateUser.execute(req);
|
|
1540
|
+
const userId = user.getId();
|
|
1541
|
+
const credentialId = req.params.id;
|
|
1542
|
+
|
|
1543
|
+
try {
|
|
1544
|
+
// Execute proxy request via credential
|
|
1545
|
+
const proxyResponse =
|
|
1546
|
+
await executeProxyRequest.executeViaCredential(
|
|
1547
|
+
credentialId,
|
|
1548
|
+
userId,
|
|
1549
|
+
req.body
|
|
1550
|
+
);
|
|
1551
|
+
|
|
1552
|
+
// Return success response
|
|
1553
|
+
res.status(200).json(proxyResponse);
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
// Handle Boom errors
|
|
1556
|
+
if (Boom.isBoom(error)) {
|
|
1557
|
+
const statusCode = error.output.statusCode;
|
|
1558
|
+
const errorData = error.data || {};
|
|
1559
|
+
|
|
1560
|
+
// Build error response matching proxyErrorResponse schema
|
|
1561
|
+
const errorResponse = {
|
|
1562
|
+
success: false,
|
|
1563
|
+
status: statusCode,
|
|
1564
|
+
error: {
|
|
1565
|
+
code:
|
|
1566
|
+
errorData.code ||
|
|
1567
|
+
_getErrorCodeFromStatus(statusCode),
|
|
1568
|
+
message:
|
|
1569
|
+
error.output.payload.message || error.message,
|
|
1570
|
+
details: errorData.details || null,
|
|
1571
|
+
},
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
// Add upstreamStatus if present
|
|
1575
|
+
if (errorData.upstreamStatus) {
|
|
1576
|
+
errorResponse.error.upstreamStatus =
|
|
1577
|
+
errorData.upstreamStatus;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
return res.status(statusCode).json(errorResponse);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// Unknown error - return 500
|
|
1584
|
+
res.status(500).json({
|
|
1585
|
+
success: false,
|
|
1586
|
+
status: 500,
|
|
1587
|
+
error: {
|
|
1588
|
+
code: 'INTERNAL_ERROR',
|
|
1589
|
+
message: 'An unexpected error occurred',
|
|
1590
|
+
details: null,
|
|
1591
|
+
},
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
})
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
// =========================================================================
|
|
1598
|
+
// v1 Legacy Aliases (backwards compatibility for singular /api/entity)
|
|
1599
|
+
// =========================================================================
|
|
1600
|
+
|
|
1601
|
+
// POST /api/entity - v1 legacy alias for POST /api/entities
|
|
1602
|
+
router.route('/api/entity').post(
|
|
1603
|
+
catchAsyncError(async (req, res) => {
|
|
1604
|
+
const user = await authenticateUser.execute(req);
|
|
1605
|
+
const userId = user.getId();
|
|
1606
|
+
const params = checkRequiredParams(req.body, [
|
|
1607
|
+
'entityType',
|
|
1608
|
+
'data',
|
|
1609
|
+
]);
|
|
1610
|
+
checkRequiredParams(req.body.data, ['credential_id']);
|
|
1611
|
+
|
|
1612
|
+
const credential = await getCredentialForUser.execute(
|
|
1613
|
+
params.data.credential_id,
|
|
1614
|
+
userId
|
|
1615
|
+
);
|
|
1616
|
+
|
|
1617
|
+
if (!credential) {
|
|
1618
|
+
throw Boom.badRequest('Invalid credential ID');
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
const module = await getModuleInstanceFromType.execute(
|
|
1622
|
+
userId,
|
|
1623
|
+
params.entityType
|
|
1624
|
+
);
|
|
1625
|
+
const entityDetails = await module.getEntityDetails(
|
|
1626
|
+
module.api,
|
|
1627
|
+
null,
|
|
1628
|
+
null,
|
|
1629
|
+
userId
|
|
1630
|
+
);
|
|
1631
|
+
|
|
1632
|
+
res.json(await module.findOrCreateEntity(entityDetails));
|
|
1633
|
+
})
|
|
1634
|
+
);
|
|
1635
|
+
|
|
1636
|
+
// GET /api/entity/options/:credentialId - v1 legacy alias
|
|
1637
|
+
router.route('/api/entity/options/:credentialId').get(
|
|
1638
|
+
catchAsyncError(async (req, res) => {
|
|
1639
|
+
const user = await authenticateUser.execute(req);
|
|
1640
|
+
const userId = user.getId();
|
|
1641
|
+
const credential = await getCredentialForUser.execute(
|
|
1642
|
+
req.params.credentialId,
|
|
1643
|
+
userId
|
|
1644
|
+
);
|
|
1645
|
+
if (credential.userId.toString() !== userId) {
|
|
1646
|
+
throw Boom.forbidden('Credential does not belong to user');
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const params = checkRequiredParams(req.query, ['entityType']);
|
|
1650
|
+
const entityOptions = await getEntityOptionsByType.execute(
|
|
1651
|
+
userId,
|
|
1652
|
+
params.entityType
|
|
1653
|
+
);
|
|
1654
|
+
|
|
1655
|
+
res.json(entityOptions);
|
|
1656
|
+
})
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
// =========================================================================
|
|
1660
|
+
// v2 API Routes - Entity endpoints (can evolve independently from v1)
|
|
1661
|
+
// =========================================================================
|
|
1662
|
+
|
|
1663
|
+
// GET /api/v2/authorize - Get authorization requirements (v2)
|
|
1664
|
+
router.route('/api/v2/authorize').get(
|
|
1665
|
+
catchAsyncError(async (req, res) => {
|
|
1666
|
+
const user = await authenticateUser.execute(req);
|
|
1667
|
+
const userId = user.getId();
|
|
1668
|
+
const params = checkRequiredParams(req.query, ['entityType']);
|
|
1669
|
+
const step = parseInt(req.query.step || '1', 10);
|
|
1670
|
+
const sessionId = req.query.sessionId;
|
|
1671
|
+
|
|
1672
|
+
if (step > 1 && !sessionId) {
|
|
1673
|
+
throw Boom.badRequest('sessionId required for step > 1');
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const requirements = await getAuthorizationRequirements.execute(
|
|
1677
|
+
params.entityType,
|
|
1678
|
+
step
|
|
1679
|
+
);
|
|
1680
|
+
|
|
1681
|
+
if (requirements.isMultiStep && step === 1 && !sessionId) {
|
|
1682
|
+
const session = await startAuthorizationSession.execute(
|
|
1683
|
+
userId,
|
|
1684
|
+
params.entityType,
|
|
1685
|
+
{
|
|
1686
|
+
step: 1,
|
|
1687
|
+
totalSteps: requirements.totalSteps,
|
|
1688
|
+
}
|
|
1689
|
+
);
|
|
1690
|
+
requirements.sessionId = session.id;
|
|
1691
|
+
} else if (sessionId) {
|
|
1692
|
+
requirements.sessionId = sessionId;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
res.json(requirements);
|
|
1696
|
+
})
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
// POST /api/v2/authorize - Process authorization callback (v2)
|
|
1700
|
+
router.route('/api/v2/authorize').post(
|
|
1701
|
+
catchAsyncError(async (req, res) => {
|
|
1702
|
+
const user = await authenticateUser.execute(req);
|
|
1703
|
+
const userId = user.getId();
|
|
1704
|
+
const params = checkRequiredParams(req.body, [
|
|
1705
|
+
'entityType',
|
|
1706
|
+
'data',
|
|
1707
|
+
]);
|
|
1708
|
+
const step = parseInt(req.body.step || '1', 10);
|
|
1709
|
+
const sessionId = req.body.sessionId;
|
|
1710
|
+
|
|
1711
|
+
if (step > 1 && !sessionId) {
|
|
1712
|
+
throw Boom.badRequest('sessionId required for step > 1');
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const result = await processAuthorizationStep.execute(
|
|
1716
|
+
userId,
|
|
1717
|
+
params.entityType,
|
|
1718
|
+
params.data,
|
|
1719
|
+
step,
|
|
1720
|
+
sessionId
|
|
1721
|
+
);
|
|
1722
|
+
|
|
1723
|
+
if (result.isComplete) {
|
|
1724
|
+
res.json({
|
|
1725
|
+
status: 'complete',
|
|
1726
|
+
entity: result.entity,
|
|
1727
|
+
credential: result.credential,
|
|
1728
|
+
});
|
|
1729
|
+
} else {
|
|
1730
|
+
res.json({
|
|
1731
|
+
status: 'pending',
|
|
1732
|
+
step: result.nextStep,
|
|
1733
|
+
totalSteps: result.totalSteps,
|
|
1734
|
+
sessionId: result.sessionId,
|
|
1735
|
+
requirements: result.requirements,
|
|
1736
|
+
message: result.message,
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
})
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
// POST /api/v2/entities - Create entity (v2)
|
|
1743
|
+
router.route('/api/v2/entities').post(
|
|
1744
|
+
catchAsyncError(async (req, res) => {
|
|
1745
|
+
const user = await authenticateUser.execute(req);
|
|
1746
|
+
const userId = user.getId();
|
|
1747
|
+
const params = checkRequiredParams(req.body, [
|
|
1748
|
+
'entityType',
|
|
1749
|
+
'data',
|
|
1750
|
+
]);
|
|
1751
|
+
checkRequiredParams(req.body.data, ['credential_id']);
|
|
1752
|
+
|
|
1753
|
+
const credential = await getCredentialForUser.execute(
|
|
1754
|
+
params.data.credential_id,
|
|
1755
|
+
userId
|
|
1756
|
+
);
|
|
1757
|
+
|
|
1758
|
+
if (!credential) {
|
|
1759
|
+
throw Boom.badRequest('Invalid credential ID');
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
const module = await getModuleInstanceFromType.execute(
|
|
1763
|
+
userId,
|
|
1764
|
+
params.entityType
|
|
1765
|
+
);
|
|
1766
|
+
const entityDetails = await module.getEntityDetails(
|
|
1767
|
+
module.api,
|
|
1768
|
+
null,
|
|
1769
|
+
null,
|
|
1770
|
+
userId
|
|
1771
|
+
);
|
|
1772
|
+
|
|
1773
|
+
res.json(await module.findOrCreateEntity(entityDetails));
|
|
1774
|
+
})
|
|
1775
|
+
);
|
|
1776
|
+
|
|
1777
|
+
// GET /api/v2/entities/types - List available entity types (v2)
|
|
1778
|
+
router.route('/api/v2/entities/types').get(
|
|
1779
|
+
catchAsyncError(async (req, res) => {
|
|
1780
|
+
await authenticateUser.execute(req);
|
|
1781
|
+
|
|
1782
|
+
const types = moduleDefinitions.map((moduleDef) => {
|
|
1783
|
+
const Definition = moduleDef.definition;
|
|
1784
|
+
return {
|
|
1785
|
+
type: moduleDef.moduleName,
|
|
1786
|
+
name:
|
|
1787
|
+
typeof Definition.getDisplayName === 'function'
|
|
1788
|
+
? Definition.getDisplayName()
|
|
1789
|
+
: moduleDef.moduleName,
|
|
1790
|
+
description:
|
|
1791
|
+
typeof Definition.getDescription === 'function'
|
|
1792
|
+
? Definition.getDescription()
|
|
1793
|
+
: undefined,
|
|
1794
|
+
authType:
|
|
1795
|
+
typeof Definition.getAuthType === 'function'
|
|
1796
|
+
? Definition.getAuthType()
|
|
1797
|
+
: 'oauth2',
|
|
1798
|
+
isMultiStep:
|
|
1799
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1800
|
+
? Definition.getAuthStepCount() > 1
|
|
1801
|
+
: false,
|
|
1802
|
+
stepCount:
|
|
1803
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1804
|
+
? Definition.getAuthStepCount()
|
|
1805
|
+
: 1,
|
|
1806
|
+
capabilities:
|
|
1807
|
+
typeof Definition.getCapabilities === 'function'
|
|
1808
|
+
? Definition.getCapabilities()
|
|
1809
|
+
: undefined,
|
|
1810
|
+
};
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
res.json({ types });
|
|
1814
|
+
})
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
// GET /api/v2/entities/types/:typeName - Get entity type details (v2)
|
|
1818
|
+
router.route('/api/v2/entities/types/:typeName').get(
|
|
1819
|
+
catchAsyncError(async (req, res) => {
|
|
1820
|
+
await authenticateUser.execute(req);
|
|
1821
|
+
const { typeName } = req.params;
|
|
1822
|
+
|
|
1823
|
+
const moduleDef = moduleDefinitions.find(
|
|
1824
|
+
(def) => def.moduleName === typeName
|
|
1825
|
+
);
|
|
1826
|
+
|
|
1827
|
+
if (!moduleDef) {
|
|
1828
|
+
throw Boom.notFound(`Entity type '${typeName}' not found`);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const Definition = moduleDef.definition;
|
|
1832
|
+
res.json({
|
|
1833
|
+
type: moduleDef.moduleName,
|
|
1834
|
+
name:
|
|
1835
|
+
typeof Definition.getDisplayName === 'function'
|
|
1836
|
+
? Definition.getDisplayName()
|
|
1837
|
+
: moduleDef.moduleName,
|
|
1838
|
+
description:
|
|
1839
|
+
typeof Definition.getDescription === 'function'
|
|
1840
|
+
? Definition.getDescription()
|
|
1841
|
+
: undefined,
|
|
1842
|
+
authType:
|
|
1843
|
+
typeof Definition.getAuthType === 'function'
|
|
1844
|
+
? Definition.getAuthType()
|
|
1845
|
+
: 'oauth2',
|
|
1846
|
+
isMultiStep:
|
|
1847
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1848
|
+
? Definition.getAuthStepCount() > 1
|
|
1849
|
+
: false,
|
|
1850
|
+
stepCount:
|
|
1851
|
+
typeof Definition.getAuthStepCount === 'function'
|
|
1852
|
+
? Definition.getAuthStepCount()
|
|
1853
|
+
: 1,
|
|
1854
|
+
capabilities:
|
|
1855
|
+
typeof Definition.getCapabilities === 'function'
|
|
1856
|
+
? Definition.getCapabilities()
|
|
1857
|
+
: undefined,
|
|
1858
|
+
});
|
|
1859
|
+
})
|
|
1860
|
+
);
|
|
1861
|
+
|
|
1862
|
+
// GET /api/v2/entities/types/:typeName/requirements - Get auth requirements (v2)
|
|
1863
|
+
router.route('/api/v2/entities/types/:typeName/requirements').get(
|
|
1864
|
+
catchAsyncError(async (req, res) => {
|
|
1865
|
+
const user = await authenticateUser.execute(req);
|
|
1866
|
+
const userId = user.getId();
|
|
1867
|
+
const { typeName } = req.params;
|
|
1868
|
+
const step = parseInt(req.query.step || '1', 10);
|
|
1869
|
+
const sessionId = req.query.sessionId;
|
|
1870
|
+
|
|
1871
|
+
if (step > 1 && !sessionId) {
|
|
1872
|
+
throw Boom.badRequest('sessionId required for step > 1');
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const requirements = await getAuthorizationRequirements.execute(
|
|
1876
|
+
typeName,
|
|
1877
|
+
step
|
|
1878
|
+
);
|
|
1879
|
+
|
|
1880
|
+
if (requirements.isMultiStep && step === 1 && !sessionId) {
|
|
1881
|
+
const session = await startAuthorizationSession.execute(
|
|
1882
|
+
userId,
|
|
1883
|
+
typeName,
|
|
1884
|
+
{
|
|
1885
|
+
step: 1,
|
|
1886
|
+
totalSteps: requirements.totalSteps,
|
|
1887
|
+
}
|
|
1888
|
+
);
|
|
1889
|
+
requirements.sessionId = session.id;
|
|
1890
|
+
} else if (sessionId) {
|
|
1891
|
+
requirements.sessionId = sessionId;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
res.json(requirements);
|
|
1895
|
+
})
|
|
1896
|
+
);
|
|
1897
|
+
|
|
1898
|
+
// GET /api/v2/entities/options/:credentialId - Get entity options (v2)
|
|
1899
|
+
router.route('/api/v2/entities/options/:credentialId').get(
|
|
1900
|
+
catchAsyncError(async (req, res) => {
|
|
1901
|
+
const user = await authenticateUser.execute(req);
|
|
1902
|
+
const userId = user.getId();
|
|
1903
|
+
const credential = await getCredentialForUser.execute(
|
|
1904
|
+
req.params.credentialId,
|
|
1905
|
+
userId
|
|
1906
|
+
);
|
|
1907
|
+
if (credential.userId.toString() !== userId) {
|
|
1908
|
+
throw Boom.forbidden('Credential does not belong to user');
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const params = checkRequiredParams(req.query, ['entityType']);
|
|
1912
|
+
const entityOptions = await getEntityOptionsByType.execute(
|
|
1913
|
+
userId,
|
|
1914
|
+
params.entityType
|
|
1915
|
+
);
|
|
1916
|
+
|
|
1917
|
+
res.json(entityOptions);
|
|
1918
|
+
})
|
|
1919
|
+
);
|
|
1920
|
+
|
|
1921
|
+
// GET /api/v2/entities/:entityId/test-auth - Test auth (v2)
|
|
1922
|
+
router.route('/api/v2/entities/:entityId/test-auth').get(
|
|
1923
|
+
catchAsyncError(async (req, res) => {
|
|
1924
|
+
const user = await authenticateUser.execute(req);
|
|
1925
|
+
const params = checkRequiredParams(req.params, ['entityId']);
|
|
1926
|
+
const testAuthResponse = await testModuleAuth.execute(
|
|
1927
|
+
params.entityId,
|
|
1928
|
+
user
|
|
1929
|
+
);
|
|
1930
|
+
|
|
1931
|
+
if (!testAuthResponse) {
|
|
1932
|
+
res.status(400);
|
|
1933
|
+
res.json({
|
|
1934
|
+
errors: [
|
|
1935
|
+
{
|
|
1936
|
+
title: 'Authentication Error',
|
|
1937
|
+
message:
|
|
1938
|
+
'There was an error with your Entity. Please reconnect/re-authenticate, or reach out to Support for assistance.',
|
|
1939
|
+
timestamp: Date.now(),
|
|
1940
|
+
},
|
|
1941
|
+
],
|
|
1942
|
+
});
|
|
1943
|
+
} else {
|
|
1944
|
+
res.json({ status: 'ok' });
|
|
1945
|
+
}
|
|
1946
|
+
})
|
|
1947
|
+
);
|
|
1948
|
+
|
|
1949
|
+
// GET /api/v2/entities/:entityId - Get entity by ID (v2)
|
|
1950
|
+
router.route('/api/v2/entities/:entityId').get(
|
|
1951
|
+
catchAsyncError(async (req, res) => {
|
|
1952
|
+
const user = await authenticateUser.execute(req);
|
|
1953
|
+
const params = checkRequiredParams(req.params, ['entityId']);
|
|
1954
|
+
const module = await getModule.execute(params.entityId, user);
|
|
1955
|
+
|
|
1956
|
+
res.json(module);
|
|
1957
|
+
})
|
|
1958
|
+
);
|
|
1959
|
+
|
|
1960
|
+
// POST /api/v2/entities/:entityId/options - Get entity options (v2)
|
|
1961
|
+
router.route('/api/v2/entities/:entityId/options').post(
|
|
1962
|
+
catchAsyncError(async (req, res) => {
|
|
1963
|
+
const user = await authenticateUser.execute(req);
|
|
1964
|
+
const params = checkRequiredParams(req.params, ['entityId']);
|
|
1965
|
+
|
|
1966
|
+
const entityOptions = await getEntityOptionsById.execute(
|
|
1967
|
+
params.entityId,
|
|
1968
|
+
user
|
|
1969
|
+
);
|
|
1970
|
+
|
|
1971
|
+
res.json(entityOptions);
|
|
1972
|
+
})
|
|
1973
|
+
);
|
|
1974
|
+
|
|
1975
|
+
// POST /api/v2/entities/:entityId/options/refresh - Refresh entity options (v2)
|
|
1976
|
+
router.route('/api/v2/entities/:entityId/options/refresh').post(
|
|
1977
|
+
catchAsyncError(async (req, res) => {
|
|
1978
|
+
const user = await authenticateUser.execute(req);
|
|
1979
|
+
const params = checkRequiredParams(req.params, ['entityId']);
|
|
1980
|
+
const updatedOptions = await refreshEntityOptions.execute(
|
|
1981
|
+
params.entityId,
|
|
1982
|
+
user,
|
|
1983
|
+
req.body
|
|
1984
|
+
);
|
|
1985
|
+
|
|
1986
|
+
res.json(updatedOptions);
|
|
1987
|
+
})
|
|
1988
|
+
);
|
|
1989
|
+
|
|
1990
|
+
// POST /api/v2/entities/:id/reauthorize - Reauthorize entity (v2)
|
|
1991
|
+
router.route('/api/v2/entities/:id/reauthorize').post(
|
|
1992
|
+
catchAsyncError(async (req, res) => {
|
|
1993
|
+
const user = await authenticateUser.execute(req);
|
|
1994
|
+
const userId = user.getId();
|
|
1995
|
+
const entityId = req.params.id;
|
|
1996
|
+
|
|
1997
|
+
const params = checkRequiredParams(req.body, ['data']);
|
|
1998
|
+
if (typeof params.data !== 'object' || Array.isArray(params.data)) {
|
|
1999
|
+
throw Boom.badRequest('data must be an object');
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
const step = parseInt(req.body.step || '1', 10);
|
|
2003
|
+
const sessionId = req.body.sessionId;
|
|
2004
|
+
|
|
2005
|
+
const entity = await moduleRepository.findEntityById(entityId);
|
|
2006
|
+
if (!entity) {
|
|
2007
|
+
throw Boom.notFound('Entity not found');
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
const credential = await credentialRepository.findCredentialById(
|
|
2011
|
+
entity.credentialId
|
|
2012
|
+
);
|
|
2013
|
+
if (!credential || credential.userId.toString() !== userId) {
|
|
2014
|
+
throw Boom.forbidden('Access denied');
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
const result = await processAuthorizationStep.execute(
|
|
2018
|
+
userId,
|
|
2019
|
+
entity.type,
|
|
2020
|
+
params.data,
|
|
2021
|
+
step,
|
|
2022
|
+
sessionId
|
|
2023
|
+
);
|
|
2024
|
+
|
|
2025
|
+
if (result.isComplete) {
|
|
2026
|
+
if (result.credential) {
|
|
2027
|
+
try {
|
|
2028
|
+
await credentialRepository.updateCredential(
|
|
2029
|
+
credential.id,
|
|
2030
|
+
{
|
|
2031
|
+
data: result.credential.data,
|
|
2032
|
+
authIsValid: true,
|
|
2033
|
+
}
|
|
2034
|
+
);
|
|
2035
|
+
await moduleRepository.updateEntity(entityId, {
|
|
2036
|
+
authIsValid: true,
|
|
2037
|
+
});
|
|
2038
|
+
} catch (error) {
|
|
2039
|
+
throw Boom.badRequest(
|
|
2040
|
+
error.message || 'Reauthorization failed'
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
res.json({
|
|
2046
|
+
step: result.nextStep,
|
|
2047
|
+
totalSteps: result.totalSteps,
|
|
2048
|
+
sessionId: result.sessionId,
|
|
2049
|
+
requirements: result.requirements,
|
|
2050
|
+
message: result.message,
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
})
|
|
2054
|
+
);
|
|
2055
|
+
|
|
2056
|
+
// POST /api/v2/entities/:id/proxy - Proxy request (v2)
|
|
2057
|
+
router.route('/api/v2/entities/:id/proxy').post(
|
|
2058
|
+
catchAsyncError(async (req, res) => {
|
|
2059
|
+
const user = await authenticateUser.execute(req);
|
|
2060
|
+
const userId = user.getId();
|
|
2061
|
+
const entityId = req.params.id;
|
|
2062
|
+
|
|
2063
|
+
try {
|
|
2064
|
+
const proxyResponse =
|
|
2065
|
+
await executeProxyRequest.executeViaEntity(
|
|
2066
|
+
entityId,
|
|
2067
|
+
userId,
|
|
2068
|
+
req.body
|
|
2069
|
+
);
|
|
2070
|
+
|
|
2071
|
+
res.status(200).json(proxyResponse);
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
if (Boom.isBoom(error)) {
|
|
2074
|
+
const statusCode = error.output.statusCode;
|
|
2075
|
+
const errorData = error.data || {};
|
|
2076
|
+
|
|
2077
|
+
const errorResponse = {
|
|
2078
|
+
success: false,
|
|
2079
|
+
status: statusCode,
|
|
2080
|
+
error: {
|
|
2081
|
+
code:
|
|
2082
|
+
errorData.code ||
|
|
2083
|
+
_getErrorCodeFromStatus(statusCode),
|
|
2084
|
+
message:
|
|
2085
|
+
error.output.payload.message || error.message,
|
|
2086
|
+
details: errorData.details || null,
|
|
2087
|
+
},
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
if (errorData.upstreamStatus) {
|
|
2091
|
+
errorResponse.error.upstreamStatus =
|
|
2092
|
+
errorData.upstreamStatus;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
return res.status(statusCode).json(errorResponse);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
res.status(500).json({
|
|
2099
|
+
success: false,
|
|
2100
|
+
status: 500,
|
|
2101
|
+
error: {
|
|
2102
|
+
code: 'INTERNAL_ERROR',
|
|
2103
|
+
message: 'An unexpected error occurred',
|
|
2104
|
+
details: null,
|
|
2105
|
+
},
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
})
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
/**
|
|
2113
|
+
* Sets up credential-related routes for the integration router
|
|
2114
|
+
* @param {Object} router - Express router instance
|
|
2115
|
+
* @param {import('../user/use-cases/authenticate-user').AuthenticateUser} authenticateUser - Authentication use case
|
|
2116
|
+
* @param {Object} useCases - Credential use cases
|
|
2117
|
+
*/
|
|
2118
|
+
function setCredentialRoutes(router, authenticateUser, useCases) {
|
|
2119
|
+
const {
|
|
2120
|
+
listCredentialsForUser,
|
|
2121
|
+
getCredentialForUser,
|
|
2122
|
+
deleteCredentialForUser,
|
|
2123
|
+
reauthorizeCredential,
|
|
2124
|
+
getAuthorizationRequirements,
|
|
2125
|
+
moduleDefinitions,
|
|
2126
|
+
} = useCases;
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* Sanitize credential object by removing sensitive data
|
|
2130
|
+
* @param {Object} credential - Credential object
|
|
2131
|
+
* @returns {Object} Sanitized credential
|
|
2132
|
+
*/
|
|
2133
|
+
function sanitizeCredential(credential) {
|
|
2134
|
+
const { data, ...safe } = credential;
|
|
2135
|
+
return safe;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// GET /api/credentials - List user's credentials
|
|
2139
|
+
router.route('/api/credentials').get(
|
|
2140
|
+
catchAsyncError(async (req, res) => {
|
|
2141
|
+
const user = await authenticateUser.execute(req);
|
|
2142
|
+
const userId = user.getId();
|
|
2143
|
+
|
|
2144
|
+
const credentials = await listCredentialsForUser.execute(userId);
|
|
2145
|
+
const sanitized = credentials.map(sanitizeCredential);
|
|
2146
|
+
|
|
2147
|
+
res.json({ credentials: sanitized });
|
|
2148
|
+
})
|
|
2149
|
+
);
|
|
2150
|
+
|
|
2151
|
+
// GET /api/credentials/:id - Get single credential
|
|
2152
|
+
router.route('/api/credentials/:id').get(
|
|
2153
|
+
catchAsyncError(async (req, res) => {
|
|
2154
|
+
const user = await authenticateUser.execute(req);
|
|
2155
|
+
const userId = user.getId();
|
|
2156
|
+
|
|
2157
|
+
const credential = await getCredentialForUser.execute(
|
|
2158
|
+
req.params.id,
|
|
2159
|
+
userId
|
|
2160
|
+
);
|
|
2161
|
+
|
|
2162
|
+
res.json(sanitizeCredential(credential));
|
|
2163
|
+
})
|
|
2164
|
+
);
|
|
2165
|
+
|
|
2166
|
+
// DELETE /api/credentials/:id - Delete credential
|
|
2167
|
+
router.route('/api/credentials/:id').delete(
|
|
2168
|
+
catchAsyncError(async (req, res) => {
|
|
2169
|
+
const user = await authenticateUser.execute(req);
|
|
2170
|
+
const userId = user.getId();
|
|
2171
|
+
|
|
2172
|
+
await deleteCredentialForUser.execute(req.params.id, userId);
|
|
2173
|
+
|
|
2174
|
+
res.json({ success: true });
|
|
2175
|
+
})
|
|
2176
|
+
);
|
|
2177
|
+
|
|
2178
|
+
// GET /api/credentials/:id/reauthorize - Get reauth requirements
|
|
2179
|
+
router.route('/api/credentials/:id/reauthorize').get(
|
|
2180
|
+
catchAsyncError(async (req, res) => {
|
|
2181
|
+
const user = await authenticateUser.execute(req);
|
|
2182
|
+
const userId = user.getId();
|
|
2183
|
+
|
|
2184
|
+
// Verify ownership
|
|
2185
|
+
const credential = await getCredentialForUser.execute(
|
|
2186
|
+
req.params.id,
|
|
2187
|
+
userId
|
|
2188
|
+
);
|
|
2189
|
+
|
|
2190
|
+
// Get authorization requirements for this credential's type
|
|
2191
|
+
const step = parseInt(req.query.step || '1', 10);
|
|
2192
|
+
const requirements = await getAuthorizationRequirements.execute(
|
|
2193
|
+
credential.type,
|
|
2194
|
+
step
|
|
2195
|
+
);
|
|
2196
|
+
|
|
2197
|
+
res.json(requirements);
|
|
2198
|
+
})
|
|
2199
|
+
);
|
|
2200
|
+
|
|
2201
|
+
// POST /api/credentials/:id/reauthorize - Submit reauth data
|
|
2202
|
+
router.route('/api/credentials/:id/reauthorize').post(
|
|
2203
|
+
catchAsyncError(async (req, res) => {
|
|
2204
|
+
const user = await authenticateUser.execute(req);
|
|
2205
|
+
const userId = user.getId();
|
|
2206
|
+
|
|
2207
|
+
const params = checkRequiredParams(req.body, ['data']);
|
|
2208
|
+
const step = parseInt(req.body.step || '1', 10);
|
|
2209
|
+
const sessionId = req.body.sessionId || null;
|
|
2210
|
+
|
|
2211
|
+
const result = await reauthorizeCredential.execute(
|
|
2212
|
+
req.params.id,
|
|
2213
|
+
userId,
|
|
2214
|
+
params.data,
|
|
2215
|
+
step,
|
|
2216
|
+
sessionId
|
|
2217
|
+
);
|
|
2218
|
+
|
|
2219
|
+
res.json(result);
|
|
2220
|
+
})
|
|
2221
|
+
);
|
|
2222
|
+
|
|
2223
|
+
// ========================================
|
|
2224
|
+
// V2 CREDENTIAL ROUTES
|
|
2225
|
+
// ========================================
|
|
2226
|
+
// These v2 routes are separate from v1 so v2 can evolve independently
|
|
2227
|
+
// while v1 remains frozen for backwards compatibility.
|
|
2228
|
+
|
|
2229
|
+
// GET /api/v2/credentials - v2: List user's credentials
|
|
2230
|
+
router.route('/api/v2/credentials').get(
|
|
2231
|
+
catchAsyncError(async (req, res) => {
|
|
2232
|
+
const user = await authenticateUser.execute(req);
|
|
2233
|
+
const userId = user.getId();
|
|
2234
|
+
|
|
2235
|
+
const credentials = await listCredentialsForUser.execute(userId);
|
|
2236
|
+
const sanitized = credentials.map(sanitizeCredential);
|
|
2237
|
+
|
|
2238
|
+
res.json({ credentials: sanitized });
|
|
2239
|
+
})
|
|
2240
|
+
);
|
|
2241
|
+
|
|
2242
|
+
// GET /api/v2/credentials/:id - v2: Get single credential
|
|
2243
|
+
router.route('/api/v2/credentials/:id').get(
|
|
2244
|
+
catchAsyncError(async (req, res) => {
|
|
2245
|
+
const user = await authenticateUser.execute(req);
|
|
2246
|
+
const userId = user.getId();
|
|
2247
|
+
|
|
2248
|
+
const credential = await getCredentialForUser.execute(
|
|
2249
|
+
req.params.id,
|
|
2250
|
+
userId
|
|
2251
|
+
);
|
|
2252
|
+
|
|
2253
|
+
res.json(sanitizeCredential(credential));
|
|
2254
|
+
})
|
|
2255
|
+
);
|
|
2256
|
+
|
|
2257
|
+
// DELETE /api/v2/credentials/:id - v2: Delete credential
|
|
2258
|
+
router.route('/api/v2/credentials/:id').delete(
|
|
2259
|
+
catchAsyncError(async (req, res) => {
|
|
2260
|
+
const user = await authenticateUser.execute(req);
|
|
2261
|
+
const userId = user.getId();
|
|
2262
|
+
|
|
2263
|
+
await deleteCredentialForUser.execute(req.params.id, userId);
|
|
2264
|
+
|
|
2265
|
+
res.json({ success: true });
|
|
2266
|
+
})
|
|
2267
|
+
);
|
|
2268
|
+
|
|
2269
|
+
// GET /api/v2/credentials/:id/reauthorize - v2: Get reauth requirements
|
|
2270
|
+
router.route('/api/v2/credentials/:id/reauthorize').get(
|
|
2271
|
+
catchAsyncError(async (req, res) => {
|
|
2272
|
+
const user = await authenticateUser.execute(req);
|
|
2273
|
+
const userId = user.getId();
|
|
2274
|
+
|
|
2275
|
+
// Verify ownership
|
|
2276
|
+
const credential = await getCredentialForUser.execute(
|
|
2277
|
+
req.params.id,
|
|
2278
|
+
userId
|
|
2279
|
+
);
|
|
2280
|
+
|
|
2281
|
+
// Get authorization requirements for this credential's type
|
|
2282
|
+
const step = parseInt(req.query.step || '1', 10);
|
|
2283
|
+
const requirements = await getAuthorizationRequirements.execute(
|
|
2284
|
+
credential.type,
|
|
2285
|
+
step
|
|
2286
|
+
);
|
|
2287
|
+
|
|
2288
|
+
res.json(requirements);
|
|
2289
|
+
})
|
|
2290
|
+
);
|
|
2291
|
+
|
|
2292
|
+
// POST /api/v2/credentials/:id/reauthorize - v2: Submit reauth data
|
|
2293
|
+
router.route('/api/v2/credentials/:id/reauthorize').post(
|
|
2294
|
+
catchAsyncError(async (req, res) => {
|
|
2295
|
+
const user = await authenticateUser.execute(req);
|
|
2296
|
+
const userId = user.getId();
|
|
2297
|
+
|
|
2298
|
+
const params = checkRequiredParams(req.body, ['data']);
|
|
2299
|
+
const step = parseInt(req.body.step || '1', 10);
|
|
2300
|
+
const sessionId = req.body.sessionId || null;
|
|
2301
|
+
|
|
2302
|
+
const result = await reauthorizeCredential.execute(
|
|
2303
|
+
req.params.id,
|
|
2304
|
+
userId,
|
|
2305
|
+
params.data,
|
|
2306
|
+
step,
|
|
2307
|
+
sessionId
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2310
|
+
res.json(result);
|
|
2311
|
+
})
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
/**
|
|
2316
|
+
* Helper function to map HTTP status codes to error codes
|
|
2317
|
+
* @private
|
|
2318
|
+
* @param {number} statusCode - HTTP status code
|
|
2319
|
+
* @returns {string} Error code
|
|
2320
|
+
*/
|
|
2321
|
+
function _getErrorCodeFromStatus(statusCode) {
|
|
2322
|
+
const statusMap = {
|
|
2323
|
+
400: 'INVALID_REQUEST',
|
|
2324
|
+
401: 'INVALID_AUTH',
|
|
2325
|
+
403: 'PERMISSION_DENIED',
|
|
2326
|
+
404: 'NOT_FOUND',
|
|
2327
|
+
429: 'RATE_LIMITED',
|
|
2328
|
+
500: 'INTERNAL_ERROR',
|
|
2329
|
+
502: 'NETWORK_ERROR',
|
|
2330
|
+
503: 'SERVICE_UNAVAILABLE',
|
|
2331
|
+
504: 'TIMEOUT',
|
|
2332
|
+
};
|
|
2333
|
+
|
|
2334
|
+
return statusMap[statusCode] || 'UNKNOWN_ERROR';
|
|
664
2335
|
}
|
|
665
2336
|
|
|
666
2337
|
module.exports = { createIntegrationRouter, checkRequiredParams };
|