@friggframework/core 2.0.0--canary.397.84ecb0e.0 → 2.0.0--canary.398.bdb6d27.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/README.md +50 -931
- package/core/create-handler.js +0 -1
- package/database/models/WebsocketConnection.js +5 -0
- package/handlers/app-handler-helpers.js +3 -0
- package/handlers/backend-utils.js +36 -49
- package/handlers/routers/auth.js +14 -3
- package/handlers/routers/integration-defined-routers.js +5 -8
- package/handlers/routers/middleware/loadUser.js +15 -0
- package/handlers/routers/middleware/requireLoggedInUser.js +12 -0
- package/handlers/routers/user.js +5 -25
- package/handlers/workers/integration-defined-workers.js +3 -6
- package/index.js +16 -1
- package/integrations/create-frigg-backend.js +31 -0
- package/integrations/index.js +5 -0
- package/integrations/integration-base.js +44 -42
- package/integrations/integration-factory.js +251 -0
- package/integrations/integration-router.js +178 -301
- package/integrations/integration-user.js +144 -0
- package/integrations/options.js +1 -1
- package/integrations/test/integration-base.test.js +144 -0
- package/module-plugin/auther.js +393 -0
- package/module-plugin/entity-manager.js +70 -0
- package/{modules → module-plugin}/index.js +8 -0
- package/module-plugin/manager.js +169 -0
- package/module-plugin/module-factory.js +61 -0
- package/{modules → module-plugin}/test/mock-api/api.js +3 -8
- package/{modules → module-plugin}/test/mock-api/definition.js +8 -12
- package/package.json +5 -5
- package/syncs/sync.js +1 -0
- package/types/integrations/index.d.ts +6 -2
- package/types/module-plugin/index.d.ts +56 -4
- package/types/syncs/index.d.ts +2 -0
- package/credential/credential-repository.js +0 -42
- package/credential/use-cases/get-credential-for-user.js +0 -21
- package/credential/use-cases/update-authentication-status.js +0 -15
- package/handlers/app-definition-loader.js +0 -38
- package/integrations/integration-repository.js +0 -80
- package/integrations/integration.js +0 -233
- package/integrations/tests/doubles/dummy-integration-class.js +0 -90
- package/integrations/tests/doubles/test-integration-repository.js +0 -89
- package/integrations/tests/use-cases/create-integration.test.js +0 -124
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -143
- package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -143
- package/integrations/tests/use-cases/get-integration-instance.test.js +0 -169
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -169
- package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
- package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
- package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
- package/integrations/tests/use-cases/update-integration.test.js +0 -134
- package/integrations/use-cases/create-integration.js +0 -72
- package/integrations/use-cases/delete-integration-for-user.js +0 -73
- package/integrations/use-cases/get-integration-for-user.js +0 -80
- package/integrations/use-cases/get-integration-instance-by-definition.js +0 -67
- package/integrations/use-cases/get-integration-instance.js +0 -84
- package/integrations/use-cases/get-integrations-for-user.js +0 -77
- package/integrations/use-cases/get-possible-integrations.js +0 -27
- package/integrations/use-cases/index.js +0 -11
- package/integrations/use-cases/update-integration-messages.js +0 -31
- package/integrations/use-cases/update-integration-status.js +0 -28
- package/integrations/use-cases/update-integration.js +0 -92
- package/integrations/utils/map-integration-dto.js +0 -36
- package/modules/module-factory.js +0 -54
- package/modules/module-repository.js +0 -107
- package/modules/module.js +0 -221
- package/modules/tests/doubles/test-module-factory.js +0 -16
- package/modules/tests/doubles/test-module-repository.js +0 -19
- package/modules/use-cases/get-entities-for-user.js +0 -32
- package/modules/use-cases/get-entity-options-by-id.js +0 -58
- package/modules/use-cases/get-entity-options-by-type.js +0 -34
- package/modules/use-cases/get-module-instance-from-type.js +0 -31
- package/modules/use-cases/get-module.js +0 -56
- package/modules/use-cases/process-authorization-callback.js +0 -114
- package/modules/use-cases/refresh-entity-options.js +0 -58
- package/modules/use-cases/test-module-auth.js +0 -54
- package/modules/utils/map-module-dto.js +0 -18
- package/user/tests/doubles/test-user-repository.js +0 -72
- package/user/tests/use-cases/create-individual-user.test.js +0 -24
- package/user/tests/use-cases/create-organization-user.test.js +0 -28
- package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
- package/user/tests/use-cases/login-user.test.js +0 -140
- package/user/use-cases/create-individual-user.js +0 -61
- package/user/use-cases/create-organization-user.js +0 -47
- package/user/use-cases/create-token-for-user-id.js +0 -30
- package/user/use-cases/get-user-from-bearer-token.js +0 -77
- package/user/use-cases/login-user.js +0 -122
- package/user/user-repository.js +0 -62
- package/user/user.js +0 -77
- /package/{modules → module-plugin}/ModuleConstants.js +0 -0
- /package/{modules → module-plugin}/credential.js +0 -0
- /package/{modules → module-plugin}/entity.js +0 -0
- /package/{modules → module-plugin}/requester/api-key.js +0 -0
- /package/{modules → module-plugin}/requester/basic.js +0 -0
- /package/{modules → module-plugin}/requester/oauth-2.js +0 -0
- /package/{modules → module-plugin}/requester/requester.js +0 -0
- /package/{modules → module-plugin}/requester/requester.test.js +0 -0
- /package/{modules → module-plugin}/test/auther.test.js +0 -0
- /package/{modules → module-plugin}/test/mock-api/mocks/hubspot.js +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const bcrypt = require('bcryptjs');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const { get } = require('../assertions');
|
|
4
|
+
const { Token } = require('../database/models/Token');
|
|
5
|
+
const { IndividualUser } = require('../database/models/IndividualUser');
|
|
6
|
+
const { OrganizationUser } = require('../database/models/OrganizationUser');
|
|
7
|
+
const Boom = require('@hapi/boom');
|
|
8
|
+
|
|
9
|
+
class User {
|
|
10
|
+
static IndividualUser = IndividualUser;
|
|
11
|
+
static OrganizationUser = OrganizationUser;
|
|
12
|
+
static Token = Token;
|
|
13
|
+
static usePassword = false
|
|
14
|
+
static primary = User.IndividualUser;
|
|
15
|
+
static individualUserRequired = true;
|
|
16
|
+
static organizationUserRequired = false;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.user = null;
|
|
20
|
+
this.individualUser = null;
|
|
21
|
+
this.organizationUser = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getPrimaryUser() {
|
|
25
|
+
if (User.primary === User.OrganizationUser) {
|
|
26
|
+
return this.organizationUser;
|
|
27
|
+
}
|
|
28
|
+
return this.individualUser;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getUserId() {
|
|
32
|
+
return this.getPrimaryUser()?.id;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isLoggedIn() {
|
|
36
|
+
return Boolean(this.getUserId());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async createUserToken(minutes) {
|
|
40
|
+
const rawToken = crypto.randomBytes(20).toString('hex');
|
|
41
|
+
const createdToken = await User.Token.createTokenWithExpire(this.getUserId(), rawToken, 120);
|
|
42
|
+
const tokenBuf = User.Token.createBase64BufferToken(createdToken, rawToken);
|
|
43
|
+
return tokenBuf;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static async newUser(params={}) {
|
|
47
|
+
const user = new User();
|
|
48
|
+
const token = get(params, 'token', null);
|
|
49
|
+
if (token) {
|
|
50
|
+
const jsonToken = this.Token.getJSONTokenFromBase64BufferToken(token);
|
|
51
|
+
const sessionToken = await this.Token.validateAndGetTokenFromJSONToken(jsonToken);
|
|
52
|
+
if (this.primary === User.OrganizationUser) {
|
|
53
|
+
user.organizationUser = await this.OrganizationUser.findById(sessionToken.user);
|
|
54
|
+
} else {
|
|
55
|
+
user.individualUser = await this.IndividualUser.findById(sessionToken.user);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return user;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static async createIndividualUser(params) {
|
|
62
|
+
const user = await this.newUser(params);
|
|
63
|
+
let hashword;
|
|
64
|
+
if (this.usePassword) {
|
|
65
|
+
hashword = get(params, 'password');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const email = get(params, 'email', null);
|
|
69
|
+
const username = get(params, 'username', null);
|
|
70
|
+
if (!email && !username) {
|
|
71
|
+
throw Boom.badRequest('email or username is required');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const appUserId = get(params, 'appUserId', null);
|
|
75
|
+
const organizationUserId = get(params, 'organizationUserId', null);
|
|
76
|
+
|
|
77
|
+
user.individualUser = await this.IndividualUser.create({
|
|
78
|
+
email,
|
|
79
|
+
username,
|
|
80
|
+
hashword,
|
|
81
|
+
appUserId,
|
|
82
|
+
organizationUser: organizationUserId,
|
|
83
|
+
});
|
|
84
|
+
return user;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static async createOrganizationUser(params) {
|
|
88
|
+
const user = await this.newUser(params);
|
|
89
|
+
const name = get(params, 'name');
|
|
90
|
+
const appOrgId = get(params, 'appOrgId');
|
|
91
|
+
user.organizationUser = await this.OrganizationUser.create({
|
|
92
|
+
name,
|
|
93
|
+
appOrgId,
|
|
94
|
+
});
|
|
95
|
+
return user;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static async loginUser(params) {
|
|
99
|
+
const user = await this.newUser(params);
|
|
100
|
+
|
|
101
|
+
if (this.usePassword){
|
|
102
|
+
const username = get(params, 'username');
|
|
103
|
+
const password = get(params, 'password');
|
|
104
|
+
|
|
105
|
+
const individualUser = await this.IndividualUser.findOne({username});
|
|
106
|
+
|
|
107
|
+
if (!individualUser) {
|
|
108
|
+
throw Boom.unauthorized('incorrect username or password');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isValid = await bcrypt.compareSync(password, individualUser.hashword);
|
|
112
|
+
if (!isValid) {
|
|
113
|
+
throw Boom.unauthorized('incorrect username or password');
|
|
114
|
+
}
|
|
115
|
+
user.individualUser = individualUser;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const appUserId = get(params, 'appUserId', null);
|
|
119
|
+
user.individualUser = await this.IndividualUser.getUserByAppUserId(
|
|
120
|
+
appUserId
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const appOrgId = get(params, 'appOrgId', null);
|
|
125
|
+
user.organizationUser = await this.OrganizationUser.getUserByAppOrgId(
|
|
126
|
+
appOrgId
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (this.individualUserRequired) {
|
|
130
|
+
if (!user.individualUser) {
|
|
131
|
+
throw Boom.unauthorized('user not found');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.organizationUserRequired) {
|
|
136
|
+
if (!user.organizationUser) {
|
|
137
|
+
throw Boom.unauthorized(`org user ${appOrgId} not found`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return user;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = User;
|
package/integrations/options.js
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
2
|
+
const { mongoose } = require('../../database/mongoose');
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const { IntegrationBase } = require("../integration-base");
|
|
5
|
+
const {Credential} = require('../../module-plugin/credential');
|
|
6
|
+
const {Entity} = require('../../module-plugin/entity');
|
|
7
|
+
const { IntegrationMapping } = require('../integration-mapping')
|
|
8
|
+
const {IntegrationModel} = require("../integration-model");
|
|
9
|
+
|
|
10
|
+
describe(`Should fully test the IntegrationBase Class`, () => {
|
|
11
|
+
let integrationRecord;
|
|
12
|
+
let userId;
|
|
13
|
+
const integration = new IntegrationBase;
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
await mongoose.connect(process.env.MONGO_URI);
|
|
17
|
+
userId = new mongoose.Types.ObjectId();
|
|
18
|
+
const credential = await Credential.findOneAndUpdate(
|
|
19
|
+
{
|
|
20
|
+
user: this.userId,
|
|
21
|
+
},
|
|
22
|
+
{ $set: { user: this.userId } },
|
|
23
|
+
{
|
|
24
|
+
new: true,
|
|
25
|
+
upsert: true,
|
|
26
|
+
setDefaultsOnInsert: true,
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
const entity1 = await Entity.findOneAndUpdate(
|
|
30
|
+
{
|
|
31
|
+
user: this.userId,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
$set: {
|
|
35
|
+
credential: credential.id,
|
|
36
|
+
user: userId,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
new: true,
|
|
41
|
+
upsert: true,
|
|
42
|
+
setDefaultsOnInsert: true,
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
const entity2 = await Entity.findOneAndUpdate(
|
|
46
|
+
{
|
|
47
|
+
user: userId,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
$set: {
|
|
51
|
+
credential: credential.id,
|
|
52
|
+
user: userId,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
new: true,
|
|
57
|
+
upsert: true,
|
|
58
|
+
setDefaultsOnInsert: true,
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
integrationRecord = await IntegrationModel.create({
|
|
62
|
+
entities: [entity1, entity2],
|
|
63
|
+
user: userId
|
|
64
|
+
});
|
|
65
|
+
integration.record = integrationRecord;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterAll(async () => {
|
|
69
|
+
await Entity.deleteMany();
|
|
70
|
+
await Credential.deleteMany();
|
|
71
|
+
await IntegrationMapping.deleteMany();
|
|
72
|
+
await IntegrationModel.deleteMany();
|
|
73
|
+
await mongoose.disconnect();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
integration.record = integrationRecord;
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('getIntegrationMapping()', () => {
|
|
81
|
+
it('should return null if not found', async () => {
|
|
82
|
+
const mappings = await integration.getMapping('badId');
|
|
83
|
+
expect(mappings).to.be.null;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return if valid ids', async () => {
|
|
87
|
+
await integration.upsertMapping('validId', {});
|
|
88
|
+
const mapping = await integration.getMapping('validId');
|
|
89
|
+
expect(mapping).to.eql({})
|
|
90
|
+
});
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('upsertIntegrationMapping()', () => {
|
|
94
|
+
it('should throw error if sourceId is null', async () => {
|
|
95
|
+
try {
|
|
96
|
+
await integration.upsertMapping( null, {});
|
|
97
|
+
fail('should have thrown error')
|
|
98
|
+
} catch(err) {
|
|
99
|
+
expect(err.message).to.contain('sourceId must be set');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return for empty mapping', async () => {
|
|
104
|
+
const mapping = await integration.upsertMapping( 'validId2', {});
|
|
105
|
+
expect(_.pick(mapping, ['integration', 'sourceId', 'mapping'])).to.eql({
|
|
106
|
+
integration: integrationRecord._id,
|
|
107
|
+
sourceId: 'validId2',
|
|
108
|
+
mapping: {}
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return for filled mapping', async () => {
|
|
113
|
+
const mapping = await integration.upsertMapping('validId3', {
|
|
114
|
+
name: 'someName',
|
|
115
|
+
value: 5
|
|
116
|
+
});
|
|
117
|
+
expect(_.pick(mapping, ['integration', 'sourceId', 'mapping'])).to.eql({
|
|
118
|
+
integration: integrationRecord._id,
|
|
119
|
+
sourceId: 'validId3',
|
|
120
|
+
mapping: {
|
|
121
|
+
name: 'someName',
|
|
122
|
+
value: 5
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should allow upserting to same id', async () => {
|
|
128
|
+
await integration.upsertMapping('validId4', {});
|
|
129
|
+
const mapping = await integration.upsertMapping('validId4', {
|
|
130
|
+
name: 'trustMe',
|
|
131
|
+
thisWorks: true,
|
|
132
|
+
});
|
|
133
|
+
expect(_.pick(mapping, ['integration', 'sourceId', 'mapping'])).to.eql({
|
|
134
|
+
integration: integrationRecord._id,
|
|
135
|
+
sourceId: 'validId4',
|
|
136
|
+
mapping: {
|
|
137
|
+
name: 'trustMe',
|
|
138
|
+
thisWorks: true,
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
});
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// Manages authorization and credential persistence
|
|
2
|
+
// Instantiation of an API Class
|
|
3
|
+
// Expects input object like this:
|
|
4
|
+
// const authDef = {
|
|
5
|
+
// API: class anAPI{},
|
|
6
|
+
// moduleName: 'anAPI', //maybe not required
|
|
7
|
+
// requiredAuthMethods: {
|
|
8
|
+
// // oauth methods, how to handle these being required/not?
|
|
9
|
+
// getToken: async function(params, callbackParams, tokenResponse) {},
|
|
10
|
+
// // required for all Auth methods
|
|
11
|
+
// getEntityDetails: async function(params) {}, //probably calls api method
|
|
12
|
+
// getCredentialDetails: async function(params) {}, // might be same as above
|
|
13
|
+
// apiParamsFromCredential: function(params) {},
|
|
14
|
+
// testAuth: async function() {}, // basic request to testAuth
|
|
15
|
+
// },
|
|
16
|
+
// env: {
|
|
17
|
+
// client_id: process.env.HUBSPOT_CLIENT_ID,
|
|
18
|
+
// client_secret: process.env.HUBSPOT_CLIENT_SECRET,
|
|
19
|
+
// scope: process.env.HUBSPOT_SCOPE,
|
|
20
|
+
// redirect_uri: `${process.env.REDIRECT_URI}/an-api`,
|
|
21
|
+
// }
|
|
22
|
+
// };
|
|
23
|
+
|
|
24
|
+
//TODO:
|
|
25
|
+
// 1. Add definition of expected params to API Class (or could just be credential?)
|
|
26
|
+
// 2.
|
|
27
|
+
|
|
28
|
+
const { Delegate } = require('../core');
|
|
29
|
+
const { get } = require('../assertions');
|
|
30
|
+
const _ = require('lodash');
|
|
31
|
+
const { flushDebugLog } = require('../logs');
|
|
32
|
+
const { Credential } = require('./credential');
|
|
33
|
+
const { Entity } = require('./entity');
|
|
34
|
+
const { mongoose } = require('../database/mongoose');
|
|
35
|
+
const { ModuleConstants } = require('./ModuleConstants');
|
|
36
|
+
|
|
37
|
+
class Auther extends Delegate {
|
|
38
|
+
static validateDefinition(definition) {
|
|
39
|
+
if (!definition) {
|
|
40
|
+
throw new Error('Auther definition is required');
|
|
41
|
+
}
|
|
42
|
+
if (!definition.moduleName) {
|
|
43
|
+
throw new Error('Auther definition requires moduleName');
|
|
44
|
+
}
|
|
45
|
+
if (!definition.API) {
|
|
46
|
+
throw new Error('Auther definition requires API class');
|
|
47
|
+
}
|
|
48
|
+
// if (!definition.Credential) {
|
|
49
|
+
// throw new Error('Auther definition requires Credential class');
|
|
50
|
+
// }
|
|
51
|
+
// if (!definition.Entity) {
|
|
52
|
+
// throw new Error('Auther definition requires Entity class');
|
|
53
|
+
// }
|
|
54
|
+
if (!definition.requiredAuthMethods) {
|
|
55
|
+
throw new Error('Auther definition requires requiredAuthMethods');
|
|
56
|
+
} else {
|
|
57
|
+
if (
|
|
58
|
+
definition.API.requesterType ===
|
|
59
|
+
ModuleConstants.authType.oauth2 &&
|
|
60
|
+
!definition.requiredAuthMethods.getToken
|
|
61
|
+
) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
'Auther definition requires requiredAuthMethods.getToken'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!definition.requiredAuthMethods.getEntityDetails) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Auther definition requires requiredAuthMethods.getEntityDetails'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (!definition.requiredAuthMethods.getCredentialDetails) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Auther definition requires requiredAuthMethods.getCredentialDetails'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!definition.requiredAuthMethods.apiPropertiesToPersist) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'Auther definition requires requiredAuthMethods.apiPropertiesToPersist'
|
|
79
|
+
);
|
|
80
|
+
} else if (definition.Credential) {
|
|
81
|
+
for (const prop of definition.requiredAuthMethods
|
|
82
|
+
.apiPropertiesToPersist?.credential) {
|
|
83
|
+
if (
|
|
84
|
+
!definition.Credential.schema.paths.hasOwnProperty(prop)
|
|
85
|
+
) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Auther definition requires Credential schema to have property ${prop}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!definition.requiredAuthMethods.testAuthRequest) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'Auther definition requires requiredAuthMethods.testAuth'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
constructor(params) {
|
|
101
|
+
super(params);
|
|
102
|
+
this.userId = get(params, 'userId', null); // Making this non-required
|
|
103
|
+
const definition = get(params, 'definition');
|
|
104
|
+
Auther.validateDefinition(definition);
|
|
105
|
+
Object.assign(this, definition.requiredAuthMethods);
|
|
106
|
+
if (definition.getEntityOptions) {
|
|
107
|
+
this.getEntityOptions = definition.getEntityOptions;
|
|
108
|
+
}
|
|
109
|
+
if (definition.refreshEntityOptions) {
|
|
110
|
+
this.refreshEntityOptions = definition.refreshEntityOptions;
|
|
111
|
+
}
|
|
112
|
+
this.name = definition.moduleName;
|
|
113
|
+
this.modelName = definition.modelName;
|
|
114
|
+
this.apiClass = definition.API;
|
|
115
|
+
this.CredentialModel =
|
|
116
|
+
definition.Credential || this.getCredentialModel();
|
|
117
|
+
this.EntityModel = definition.Entity || this.getEntityModel();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static async getInstance(params) {
|
|
121
|
+
const instance = new this(params);
|
|
122
|
+
if (params.entityId) {
|
|
123
|
+
instance.entity = await instance.EntityModel.findById(
|
|
124
|
+
params.entityId
|
|
125
|
+
);
|
|
126
|
+
instance.credential = await instance.CredentialModel.findById(
|
|
127
|
+
instance.entity.credential
|
|
128
|
+
);
|
|
129
|
+
} else if (params.credentialId) {
|
|
130
|
+
instance.credential = await instance.CredentialModel.findById(
|
|
131
|
+
params.credentialId
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
let credential = {};
|
|
135
|
+
let entity = {};
|
|
136
|
+
if (instance.credential) {
|
|
137
|
+
credential = instance.credential.toObject();
|
|
138
|
+
}
|
|
139
|
+
if (instance.entity) {
|
|
140
|
+
entity = instance.entity.toObject();
|
|
141
|
+
}
|
|
142
|
+
const apiParams = {
|
|
143
|
+
...params.definition.env,
|
|
144
|
+
delegate: instance,
|
|
145
|
+
...instance.apiParamsFromCredential(credential),
|
|
146
|
+
...instance.apiParamsFromEntity(entity),
|
|
147
|
+
};
|
|
148
|
+
instance.api = new instance.apiClass(apiParams);
|
|
149
|
+
return instance;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static getEntityModelFromDefinition(definition) {
|
|
153
|
+
const partialModule = new this({ definition });
|
|
154
|
+
return partialModule.getEntityModel();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getName() {
|
|
158
|
+
return this.name;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
apiParamsFromCredential(credential) {
|
|
162
|
+
return _.pick(credential, ...this.apiPropertiesToPersist?.credential);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
apiParamsFromEntity(entity) {
|
|
166
|
+
return _.pick(entity, ...this.apiPropertiesToPersist?.entity);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getEntityModel() {
|
|
170
|
+
if (!this.EntityModel) {
|
|
171
|
+
const prefix = this.modelName ?? _.upperFirst(this.getName());
|
|
172
|
+
const arrayToDefaultObject = (array, defaultValue) =>
|
|
173
|
+
_.mapValues(_.keyBy(array), () => defaultValue);
|
|
174
|
+
const schema = new mongoose.Schema(
|
|
175
|
+
arrayToDefaultObject(this.apiPropertiesToPersist.entity, {
|
|
176
|
+
type: mongoose.Schema.Types.Mixed,
|
|
177
|
+
trim: true,
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
const name = `${prefix}Entity`;
|
|
181
|
+
this.EntityModel =
|
|
182
|
+
Entity.discriminators?.[name] ||
|
|
183
|
+
Entity.discriminator(name, schema);
|
|
184
|
+
}
|
|
185
|
+
return this.EntityModel;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getCredentialModel() {
|
|
189
|
+
if (!this.CredentialModel) {
|
|
190
|
+
const arrayToDefaultObject = (array, defaultValue) =>
|
|
191
|
+
_.mapValues(_.keyBy(array), () => defaultValue);
|
|
192
|
+
const schema = new mongoose.Schema(
|
|
193
|
+
arrayToDefaultObject(this.apiPropertiesToPersist.credential, {
|
|
194
|
+
type: mongoose.Schema.Types.Mixed,
|
|
195
|
+
trim: true,
|
|
196
|
+
lhEncrypt: true,
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
const prefix = this.modelName ?? _.upperFirst(this.getName());
|
|
200
|
+
const name = `${prefix}Credential`;
|
|
201
|
+
this.CredentialModel =
|
|
202
|
+
Credential.discriminators?.[name] ||
|
|
203
|
+
Credential.discriminator(name, schema);
|
|
204
|
+
}
|
|
205
|
+
return this.CredentialModel;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async getEntitiesForUserId(userId) {
|
|
209
|
+
// Only return non-internal fields. Leverages "select" and "options" to non-excepted fields and a pure object.
|
|
210
|
+
const list = await this.EntityModel.find(
|
|
211
|
+
{ user: userId },
|
|
212
|
+
'-dateCreated -dateUpdated -user -credentials -credential -__t -__v',
|
|
213
|
+
{ lean: true }
|
|
214
|
+
);
|
|
215
|
+
console.log('getEntitiesForUserId list', list, userId);
|
|
216
|
+
return list.map((entity) => ({
|
|
217
|
+
id: entity._id,
|
|
218
|
+
type: this.getName(),
|
|
219
|
+
...entity,
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async validateAuthorizationRequirements() {
|
|
224
|
+
const requirements = await this.getAuthorizationRequirements();
|
|
225
|
+
let valid = true;
|
|
226
|
+
if (
|
|
227
|
+
['oauth1', 'oauth2'].includes(requirements.type) &&
|
|
228
|
+
!requirements.url
|
|
229
|
+
) {
|
|
230
|
+
valid = false;
|
|
231
|
+
}
|
|
232
|
+
return valid;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async getAuthorizationRequirements(params) {
|
|
236
|
+
// TODO: How can this be more helpful both to implement and consume
|
|
237
|
+
// this function must return a dictionary with the following format
|
|
238
|
+
// node only url key is required. Data would be used for Base Authentication
|
|
239
|
+
// let returnData = {
|
|
240
|
+
// url: "callback url for the data or teh redirect url for login",
|
|
241
|
+
// type: one of the types defined in modules/Constants.js
|
|
242
|
+
// data: ["required", "fields", "we", "may", "need"]
|
|
243
|
+
// }
|
|
244
|
+
return this.api.getAuthorizationRequirements();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async testAuth(params) {
|
|
248
|
+
let validAuth = false;
|
|
249
|
+
try {
|
|
250
|
+
if (await this.testAuthRequest(this.api)) validAuth = true;
|
|
251
|
+
} catch (e) {
|
|
252
|
+
flushDebugLog(e);
|
|
253
|
+
}
|
|
254
|
+
return validAuth;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async processAuthorizationCallback(params) {
|
|
258
|
+
let tokenResponse;
|
|
259
|
+
if (this.apiClass.requesterType === ModuleConstants.authType.oauth2) {
|
|
260
|
+
tokenResponse = await this.getToken(this.api, params);
|
|
261
|
+
} else {
|
|
262
|
+
tokenResponse = await this.setAuthParams(this.api, params);
|
|
263
|
+
await this.onTokenUpdate();
|
|
264
|
+
}
|
|
265
|
+
const authRes = await this.testAuth();
|
|
266
|
+
if (!authRes) {
|
|
267
|
+
throw new Error('Authorization failed');
|
|
268
|
+
}
|
|
269
|
+
const entityDetails = await this.getEntityDetails(
|
|
270
|
+
this.api,
|
|
271
|
+
params,
|
|
272
|
+
tokenResponse,
|
|
273
|
+
this.userId
|
|
274
|
+
);
|
|
275
|
+
Object.assign(
|
|
276
|
+
entityDetails.details,
|
|
277
|
+
this.apiParamsFromEntity(this.api)
|
|
278
|
+
);
|
|
279
|
+
await this.findOrCreateEntity(entityDetails);
|
|
280
|
+
return {
|
|
281
|
+
credential_id: this.credential.id,
|
|
282
|
+
entity_id: this.entity.id,
|
|
283
|
+
type: this.getName(),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async onTokenUpdate() {
|
|
288
|
+
const credentialDetails = await this.getCredentialDetails(
|
|
289
|
+
this.api,
|
|
290
|
+
this.userId
|
|
291
|
+
);
|
|
292
|
+
Object.assign(
|
|
293
|
+
credentialDetails.details,
|
|
294
|
+
this.apiParamsFromCredential(this.api)
|
|
295
|
+
);
|
|
296
|
+
credentialDetails.details.auth_is_valid = true;
|
|
297
|
+
await this.updateOrCreateCredential(credentialDetails);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async receiveNotification(notifier, delegateString, object = null) {
|
|
301
|
+
if (delegateString === this.api.DLGT_TOKEN_UPDATE) {
|
|
302
|
+
await this.onTokenUpdate();
|
|
303
|
+
} else if (delegateString === this.api.DLGT_TOKEN_DEAUTHORIZED) {
|
|
304
|
+
await this.deauthorize();
|
|
305
|
+
} else if (delegateString === this.api.DLGT_INVALID_AUTH) {
|
|
306
|
+
await this.markCredentialsInvalid();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async getEntityOptions() {
|
|
311
|
+
throw new Error(
|
|
312
|
+
'Method getEntityOptions() is not defined in the class'
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async refreshEntityOptions() {
|
|
317
|
+
throw new Error(
|
|
318
|
+
'Method refreshEntityOptions() is not defined in the class'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async findOrCreateEntity(entityDetails) {
|
|
323
|
+
const identifiers = get(entityDetails, 'identifiers');
|
|
324
|
+
const details = get(entityDetails, 'details');
|
|
325
|
+
const search = await this.EntityModel.find(identifiers);
|
|
326
|
+
if (search.length > 1) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
'Multiple entities found with the same identifiers: ' +
|
|
329
|
+
JSON.stringify(identifiers)
|
|
330
|
+
);
|
|
331
|
+
} else if (search.length === 0) {
|
|
332
|
+
this.entity = await this.EntityModel.create({
|
|
333
|
+
credential: this.credential.id,
|
|
334
|
+
...details,
|
|
335
|
+
...identifiers,
|
|
336
|
+
});
|
|
337
|
+
} else if (search.length === 1) {
|
|
338
|
+
this.entity = search[0];
|
|
339
|
+
}
|
|
340
|
+
if (this.entity.credential === undefined) {
|
|
341
|
+
this.entity.credential = this.credential.id;
|
|
342
|
+
await this.entity.save();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async updateOrCreateCredential(credentialDetails) {
|
|
347
|
+
const identifiers = get(credentialDetails, 'identifiers');
|
|
348
|
+
const details = get(credentialDetails, 'details');
|
|
349
|
+
|
|
350
|
+
if (!this.credential) {
|
|
351
|
+
const credentialSearch = await this.CredentialModel.find(
|
|
352
|
+
identifiers
|
|
353
|
+
);
|
|
354
|
+
if (credentialSearch.length > 1) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Multiple credentials found with same identifiers: ${identifiers}`
|
|
357
|
+
);
|
|
358
|
+
} else if (credentialSearch.length === 1) {
|
|
359
|
+
// found exactly one credential with these identifiers
|
|
360
|
+
this.credential = credentialSearch[0];
|
|
361
|
+
} else {
|
|
362
|
+
// found no credential with these identifiers (match none for insert)
|
|
363
|
+
this.credential = { $exists: false };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// update credential or create if none was found
|
|
367
|
+
this.credential = await this.CredentialModel.findOneAndUpdate(
|
|
368
|
+
{ _id: this.credential },
|
|
369
|
+
{ $set: { ...identifiers, ...details } },
|
|
370
|
+
{ useFindAndModify: true, new: true, upsert: true }
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async markCredentialsInvalid() {
|
|
375
|
+
if (this.credential) {
|
|
376
|
+
this.credential.auth_is_valid = false;
|
|
377
|
+
await this.credential.save();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async deauthorize() {
|
|
382
|
+
this.api = new this.apiClass();
|
|
383
|
+
if (this.entity?.credential) {
|
|
384
|
+
await this.CredentialModel.deleteOne({
|
|
385
|
+
_id: this.entity.credential,
|
|
386
|
+
});
|
|
387
|
+
this.entity.credential = undefined;
|
|
388
|
+
await this.entity.save();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = { Auther };
|