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