@memberjunction/server 2.1.5 → 2.2.1

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.
Files changed (149) hide show
  1. package/dist/apolloServer/TransactionPlugin.d.ts +1 -1
  2. package/dist/apolloServer/TransactionPlugin.d.ts.map +1 -1
  3. package/dist/apolloServer/TransactionPlugin.js.map +1 -1
  4. package/dist/apolloServer/index.d.ts +1 -1
  5. package/dist/apolloServer/index.d.ts.map +1 -1
  6. package/dist/apolloServer/index.js +2 -2
  7. package/dist/apolloServer/index.js.map +1 -1
  8. package/dist/auth/exampleNewUserSubClass.d.ts +1 -1
  9. package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
  10. package/dist/auth/exampleNewUserSubClass.js +7 -7
  11. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  12. package/dist/auth/index.d.ts +1 -1
  13. package/dist/auth/index.d.ts.map +1 -1
  14. package/dist/auth/index.js +18 -8
  15. package/dist/auth/index.js.map +1 -1
  16. package/dist/auth/newUsers.js +1 -1
  17. package/dist/auth/newUsers.js.map +1 -1
  18. package/dist/context.d.ts +1 -1
  19. package/dist/context.d.ts.map +1 -1
  20. package/dist/context.js +4 -4
  21. package/dist/context.js.map +1 -1
  22. package/dist/directives/Public.d.ts +1 -1
  23. package/dist/directives/Public.d.ts.map +1 -1
  24. package/dist/directives/index.d.ts +1 -1
  25. package/dist/directives/index.d.ts.map +1 -1
  26. package/dist/directives/index.js +1 -1
  27. package/dist/directives/index.js.map +1 -1
  28. package/dist/entitySubclasses/entityPermissions.server.d.ts +1 -1
  29. package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
  30. package/dist/entitySubclasses/entityPermissions.server.js +5 -6
  31. package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
  32. package/dist/generated/generated.d.ts +14 -2
  33. package/dist/generated/generated.d.ts.map +1 -1
  34. package/dist/generated/generated.js +79 -18
  35. package/dist/generated/generated.js.map +1 -1
  36. package/dist/generic/ResolverBase.d.ts +3 -3
  37. package/dist/generic/ResolverBase.d.ts.map +1 -1
  38. package/dist/generic/ResolverBase.js +1 -1
  39. package/dist/generic/ResolverBase.js.map +1 -1
  40. package/dist/generic/RunViewResolver.d.ts +2 -2
  41. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  42. package/dist/generic/RunViewResolver.js +1 -1
  43. package/dist/generic/RunViewResolver.js.map +1 -1
  44. package/dist/index.d.ts +21 -21
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +36 -27
  47. package/dist/index.js.map +1 -1
  48. package/dist/orm.js +4 -4
  49. package/dist/orm.js.map +1 -1
  50. package/dist/resolvers/AskSkipResolver.d.ts +3 -3
  51. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  52. package/dist/resolvers/AskSkipResolver.js +64 -59
  53. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  54. package/dist/resolvers/ColorResolver.d.ts +1 -1
  55. package/dist/resolvers/ColorResolver.d.ts.map +1 -1
  56. package/dist/resolvers/ColorResolver.js +1 -1
  57. package/dist/resolvers/ColorResolver.js.map +1 -1
  58. package/dist/resolvers/DatasetResolver.d.ts +1 -1
  59. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  60. package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -2
  61. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  62. package/dist/resolvers/EntityCommunicationsResolver.js +9 -4
  63. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  64. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -2
  65. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  66. package/dist/resolvers/EntityRecordNameResolver.js +2 -2
  67. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  68. package/dist/resolvers/EntityResolver.d.ts +2 -2
  69. package/dist/resolvers/EntityResolver.d.ts.map +1 -1
  70. package/dist/resolvers/EntityResolver.js +1 -1
  71. package/dist/resolvers/EntityResolver.js.map +1 -1
  72. package/dist/resolvers/FileCategoryResolver.d.ts +1 -1
  73. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  74. package/dist/resolvers/FileCategoryResolver.js +2 -2
  75. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  76. package/dist/resolvers/FileResolver.d.ts +2 -2
  77. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  78. package/dist/resolvers/FileResolver.js +3 -3
  79. package/dist/resolvers/FileResolver.js.map +1 -1
  80. package/dist/resolvers/MergeRecordsResolver.d.ts +2 -2
  81. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  82. package/dist/resolvers/MergeRecordsResolver.js +4 -2
  83. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  84. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +2 -2
  85. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
  86. package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
  87. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  88. package/dist/resolvers/QueryResolver.d.ts +1 -1
  89. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  90. package/dist/resolvers/ReportResolver.d.ts +1 -1
  91. package/dist/resolvers/ReportResolver.d.ts.map +1 -1
  92. package/dist/resolvers/ReportResolver.js +16 -14
  93. package/dist/resolvers/ReportResolver.js.map +1 -1
  94. package/dist/resolvers/UserFavoriteResolver.d.ts +1 -1
  95. package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
  96. package/dist/resolvers/UserFavoriteResolver.js +17 -16
  97. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  98. package/dist/resolvers/UserResolver.d.ts +1 -1
  99. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  100. package/dist/resolvers/UserResolver.js +1 -1
  101. package/dist/resolvers/UserResolver.js.map +1 -1
  102. package/dist/resolvers/UserViewResolver.d.ts +1 -1
  103. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  104. package/dist/resolvers/UserViewResolver.js +2 -2
  105. package/dist/resolvers/UserViewResolver.js.map +1 -1
  106. package/dist/util.d.ts.map +1 -1
  107. package/dist/util.js +11 -5
  108. package/dist/util.js.map +1 -1
  109. package/package.json +27 -23
  110. package/src/apolloServer/TransactionPlugin.ts +53 -0
  111. package/src/apolloServer/index.ts +33 -0
  112. package/src/auth/exampleNewUserSubClass.ts +79 -0
  113. package/src/auth/index.ts +171 -0
  114. package/src/auth/newUsers.ts +58 -0
  115. package/src/auth/tokenExpiredError.ts +12 -0
  116. package/src/cache.ts +10 -0
  117. package/src/config.ts +89 -0
  118. package/src/context.ts +111 -0
  119. package/src/directives/Public.ts +42 -0
  120. package/src/directives/index.ts +1 -0
  121. package/src/entitySubclasses/DuplicateRunEntity.server.ts +29 -0
  122. package/src/entitySubclasses/entityPermissions.server.ts +104 -0
  123. package/src/entitySubclasses/userViewEntity.server.ts +187 -0
  124. package/src/generated/generated.ts +25406 -0
  125. package/src/generic/DeleteOptionsInput.ts +13 -0
  126. package/src/generic/KeyInputOutputTypes.ts +35 -0
  127. package/src/generic/KeyValuePairInput.ts +14 -0
  128. package/src/generic/PushStatusResolver.ts +40 -0
  129. package/src/generic/ResolverBase.ts +767 -0
  130. package/src/generic/RunViewResolver.ts +579 -0
  131. package/src/index.ts +171 -0
  132. package/src/orm.ts +36 -0
  133. package/src/resolvers/AskSkipResolver.ts +1112 -0
  134. package/src/resolvers/ColorResolver.ts +61 -0
  135. package/src/resolvers/DatasetResolver.ts +115 -0
  136. package/src/resolvers/EntityCommunicationsResolver.ts +221 -0
  137. package/src/resolvers/EntityRecordNameResolver.ts +75 -0
  138. package/src/resolvers/EntityResolver.ts +35 -0
  139. package/src/resolvers/FileCategoryResolver.ts +69 -0
  140. package/src/resolvers/FileResolver.ts +152 -0
  141. package/src/resolvers/MergeRecordsResolver.ts +175 -0
  142. package/src/resolvers/PotentialDuplicateRecordResolver.ts +91 -0
  143. package/src/resolvers/QueryResolver.ts +42 -0
  144. package/src/resolvers/ReportResolver.ts +144 -0
  145. package/src/resolvers/UserFavoriteResolver.ts +176 -0
  146. package/src/resolvers/UserResolver.ts +33 -0
  147. package/src/resolvers/UserViewResolver.ts +64 -0
  148. package/src/types.ts +40 -0
  149. package/src/util.ts +112 -0
@@ -0,0 +1,171 @@
1
+ import { JwtHeader, SigningKeyCallback } from 'jsonwebtoken';
2
+ import jwksClient from 'jwks-rsa';
3
+ import { auth0Domain, auth0WebClientID, configInfo, tenantID, webClientID } from '../config.js';
4
+ import { UserCache } from '@memberjunction/sqlserver-dataprovider';
5
+ import { DataSource } from 'typeorm';
6
+ import { Metadata, UserInfo } from '@memberjunction/core';
7
+ import { NewUserBase } from './newUsers.js';
8
+ import { MJGlobal } from '@memberjunction/global';
9
+
10
+ export { TokenExpiredError } from './tokenExpiredError.js';
11
+
12
+ const missingAzureConfig = !tenantID || !webClientID;
13
+ const missingAuth0Config = !auth0Domain || !auth0WebClientID;
14
+
15
+ class MissingAuthError extends Error {
16
+ constructor() {
17
+ super('Could not find authentication configuration for either MSAL or Auth0 in the server environment variables.');
18
+ this.name = 'MissingAuthError';
19
+ }
20
+ }
21
+
22
+ const issuers = {
23
+ azure: `https://login.microsoftonline.com/${tenantID}/v2.0`,
24
+ auth0: `https://${auth0Domain}/`,
25
+ };
26
+
27
+ export const validationOptions = {
28
+ [issuers.auth0]: {
29
+ audience: auth0WebClientID,
30
+ jwksUri: `https://${auth0Domain}/.well-known/jwks.json`,
31
+ },
32
+ [issuers.azure]: {
33
+ audience: webClientID,
34
+ jwksUri: `https://login.microsoftonline.com/${tenantID}/discovery/v2.0/keys`,
35
+ },
36
+ };
37
+
38
+ export class UserPayload {
39
+ aio?: string;
40
+ aud?: string;
41
+ exp?: number;
42
+ iat?: number;
43
+ iss?: string;
44
+ name?: string;
45
+ nbf?: number;
46
+ nonce?: string;
47
+ oid?: string;
48
+ preferred_username?: string;
49
+ rh?: string;
50
+ sub?: string;
51
+ tid?: string;
52
+ uti?: string;
53
+ ver?: string;
54
+ // what about an array of roles???
55
+ }
56
+
57
+ export const getSigningKeys = (issuer: string) => (header: JwtHeader, cb: SigningKeyCallback) => {
58
+ if (!validationOptions[issuer]) {
59
+ throw new Error(`No validation options found for issuer ${issuer}`);
60
+ }
61
+
62
+ const jwksUri = validationOptions[issuer].jwksUri;
63
+ if (missingAuth0Config && missingAzureConfig) {
64
+ throw new MissingAuthError();
65
+ }
66
+ if (missingAuth0Config) {
67
+ console.warn('Auth0 configuration not found in environment variables');
68
+ }
69
+ if (missingAzureConfig) {
70
+ console.warn('MSAL configuration not found in environment variables');
71
+ }
72
+
73
+ jwksClient({ jwksUri })
74
+ .getSigningKey(header.kid)
75
+ .then((key) => {
76
+ cb(null, 'publicKey' in key ? key.publicKey : key.rsaPublicKey);
77
+ })
78
+ .catch((err) => console.error(err));
79
+ };
80
+
81
+ export const verifyUserRecord = async (
82
+ email?: string,
83
+ firstName?: string,
84
+ lastName?: string,
85
+ requestDomain?: string,
86
+ dataSource?: DataSource,
87
+ attemptCacheUpdateIfNeeded: boolean = true
88
+ ): Promise<UserInfo | undefined> => {
89
+ if (!email) return undefined;
90
+
91
+ let user = UserCache.Instance.Users.find((u) => {
92
+ if (!u.Email || u.Email.trim() === '') {
93
+ // this condition should never occur. If it doesn throw a console error including the user id
94
+ // DB requires non-null but this is just an extra check and we could in theory have a blank string in the DB
95
+ console.error(`SYSTEM METADATA ISSUE: User ${u.ID} has no email address`);
96
+ return false;
97
+ } else return u.Email.toLowerCase().trim() === email.toLowerCase().trim();
98
+ });
99
+
100
+ if (!user) {
101
+ if (
102
+ configInfo.userHandling.autoCreateNewUsers &&
103
+ firstName &&
104
+ lastName &&
105
+ (requestDomain || configInfo.userHandling.newUserLimitedToAuthorizedDomains === false)
106
+ ) {
107
+ // check to see if the domain that we have a request coming in from matches one of the domains in the autoCreateNewUsersDomains setting
108
+ let passesDomainCheck: boolean =
109
+ configInfo.userHandling.newUserLimitedToAuthorizedDomains ===
110
+ false; /*in this first condition, we are set up to NOT care about domain */
111
+ if (!passesDomainCheck && requestDomain) {
112
+ /*in this second condition, we check the domain against authorized domains*/
113
+ passesDomainCheck = configInfo.userHandling.newUserAuthorizedDomains.some((pattern) => {
114
+ // Convert wildcard domain patterns to regular expressions
115
+ const regex = new RegExp('^' + pattern.toLowerCase().trim().replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
116
+ return regex.test(requestDomain?.toLowerCase().trim());
117
+ });
118
+ }
119
+
120
+ if (passesDomainCheck) {
121
+ // we have a domain from the request that matches one of the domains provided by the configuration, so we will create a new user
122
+ console.warn(`User ${email} not found in cache. Attempting to create a new user...`);
123
+ const newUserCreator: NewUserBase = <NewUserBase>MJGlobal.Instance.ClassFactory.CreateInstance(NewUserBase); // this will create the object that handles creating the new user for us
124
+ const newUser = await newUserCreator.createNewUser(firstName, lastName, email);
125
+ if (newUser) {
126
+ // new user worked! we already have the stuff we need for the cache, so no need to go to the DB now, just create a new UserInfo object and use the return value from the createNewUser method
127
+ // to init it, including passing in the role list for the user.
128
+ const initData: any = newUser.GetAll();
129
+ initData.UserRoles = configInfo.userHandling.newUserRoles.map((role) => {
130
+ return { UserID: initData.ID, RoleName: role };
131
+ });
132
+ user = new UserInfo(Metadata.Provider, initData);
133
+ UserCache.Instance.Users.push(user);
134
+ console.warn(` >>> New user ${email} created successfully!`);
135
+ }
136
+ } else {
137
+ console.warn(
138
+ `User ${email} not found in cache. Request domain '${requestDomain}' does not match any of the domains in the newUserAuthorizedDomains setting. To ignore domain, make sure you set the newUserLimitedToAuthorizedDomains setting to false. In this case we are NOT creating a new user.`
139
+ );
140
+ }
141
+ }
142
+ if (!user && configInfo.userHandling.updateCacheWhenNotFound && dataSource && attemptCacheUpdateIfNeeded) {
143
+ // if we get here that means in the above, if we were attempting to create a new user, it did not work, or it wasn't attempted and we have a config that asks us to auto update the cache
144
+ console.warn(`User ${email} not found in cache. Updating cache in attempt to find the user...`);
145
+
146
+ const startTime: number = Date.now();
147
+ await UserCache.Instance.Refresh(dataSource);
148
+ const endTime: number = Date.now();
149
+ const elapsed: number = endTime - startTime;
150
+
151
+ // if elapsed time is less than the delay setting, wait for the additional time to achieve the full delay
152
+ // the below also makes sure we never go more than a 30 second total delay
153
+ const delay = configInfo.userHandling.updateCacheWhenNotFoundDelay
154
+ ? configInfo.userHandling.updateCacheWhenNotFoundDelay < 30000
155
+ ? configInfo.userHandling.updateCacheWhenNotFoundDelay
156
+ : 30000
157
+ : 0;
158
+ if (elapsed < delay) await new Promise((resolve) => setTimeout(resolve, delay - elapsed));
159
+
160
+ const finalTime: number = Date.now();
161
+ const finalElapsed: number = finalTime - startTime;
162
+
163
+ console.log(
164
+ ` UserCache updated in ${elapsed}ms, total elapsed time of ${finalElapsed}ms including delay of ${delay}ms (if needed). Attempting to find the user again via recursive call to verifyUserRecord()`
165
+ );
166
+ return verifyUserRecord(email, firstName, lastName, requestDomain, dataSource, false); // try one more time but do not update cache next time if not found
167
+ }
168
+ }
169
+
170
+ return user;
171
+ };
@@ -0,0 +1,58 @@
1
+ import { LogError, Metadata } from "@memberjunction/core";
2
+ import { RegisterClass } from "@memberjunction/global";
3
+ import { UserCache } from "@memberjunction/sqlserver-dataprovider";
4
+ import { configInfo } from "../config.js";
5
+ import { UserEntity, UserRoleEntity } from "@memberjunction/core-entities";
6
+
7
+ @RegisterClass(NewUserBase)
8
+ export class NewUserBase {
9
+ public async createNewUser(firstName: string, lastName: string, email: string, linkedRecordType: string = 'None', linkedEntityId?: string, linkedEntityRecordId?: string) {
10
+ try {
11
+ const md = new Metadata();
12
+ const contextUser = UserCache.Instance.Users.find(u => u.Email.trim().toLowerCase() === configInfo?.userHandling?.contextUserForNewUserCreation?.trim().toLowerCase())
13
+ if (!contextUser) {
14
+ LogError(`Failed to load context user ${configInfo?.userHandling?.contextUserForNewUserCreation}, if you've not specified this on your config.json you must do so. This is the user that is contextually used for creating a new user record dynamically.`);
15
+ return undefined;
16
+ }
17
+ const u = <UserEntity>await md.GetEntityObject('Users', contextUser) // To-Do - change this to be a different defined user for the user creation process
18
+ u.NewRecord();
19
+ u.Name = email;
20
+ u.IsActive = true;
21
+ u.FirstName = firstName;
22
+ u.LastName = lastName;
23
+ u.Email = email;
24
+ u.Type = 'User';
25
+ u.LinkedRecordType = linkedRecordType;
26
+ if (linkedEntityId)
27
+ u.LinkedEntityID = linkedEntityId;
28
+ if (linkedEntityRecordId)
29
+ u.LinkedEntityRecordID = linkedEntityRecordId;
30
+
31
+ if (await u.Save()) {
32
+ // user created, now create however many roles we need to create for this user based on the config settings
33
+ const ur = await md.GetEntityObject<UserRoleEntity>('User Roles', contextUser);
34
+ let bSuccess: boolean = true;
35
+ for (const role of configInfo.userHandling.newUserRoles) {
36
+ ur.NewRecord();
37
+ ur.UserID = u.ID;
38
+ const roleID = md.Roles.find(r => r.Name === role)?.ID;
39
+ ur.RoleID = roleID;
40
+ bSuccess = bSuccess && await ur.Save();
41
+ }
42
+ if (!bSuccess) {
43
+ LogError(`Failed to create roles for newly created user ${firstName} ${lastName} ${email}`);
44
+ return undefined;
45
+ }
46
+ }
47
+ else {
48
+ LogError(`Failed to create new user ${firstName} ${lastName} ${email}`);
49
+ return undefined;
50
+ }
51
+ return u;
52
+ }
53
+ catch (e) {
54
+ LogError(e);
55
+ return undefined;
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,12 @@
1
+ import { GraphQLError } from 'graphql';
2
+
3
+ export class TokenExpiredError extends GraphQLError {
4
+ constructor(expiryDate: Date, message = 'The provided token has expired. Please authenticate again.') {
5
+ super(message, {
6
+ extensions: {
7
+ code: 'JWT_EXPIRED',
8
+ expiryDate: expiryDate.toISOString(),
9
+ },
10
+ });
11
+ }
12
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { LRUCache } from 'lru-cache';
2
+
3
+ const oneHourMs = 60 * 60 * 1000;
4
+
5
+ export const authCache = new LRUCache({
6
+ max: 50000,
7
+ ttl: oneHourMs,
8
+ ttlAutopurge: false,
9
+ });
10
+
package/src/config.ts ADDED
@@ -0,0 +1,89 @@
1
+ import env from 'env-var';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { z } from 'zod';
5
+
6
+ export const nodeEnv = env.get('NODE_ENV').asString();
7
+
8
+ export const dbHost = env.get('DB_HOST').required().asString();
9
+ export const dbPort = env.get('DB_PORT').default('1433').asPortNumber();
10
+ export const dbUsername = env.get('DB_USERNAME').required().asString();
11
+ export const dbPassword = env.get('DB_PASSWORD').required().asString();
12
+ export const dbDatabase = env.get('DB_DATABASE').required().asString();
13
+ export const dbInstanceName = env.get('DB_INSTANCE_NAME').asString();
14
+ export const dbTrustServerCertificate = env.get('DB_TRUST_SERVER_CERTIFICATE').asBool();
15
+
16
+ export const graphqlPort = env.get('PORT').default('4000').asPortNumber();
17
+
18
+ export const ___codeGenAPIURL = env.get('CODEGEN_API_URL').asString();
19
+ export const ___codeGenAPIPort = env.get('CODEGEN_API_PORT').default('3999').asPortNumber();
20
+ export const ___codeGenAPISubmissionDelay = env.get('CODEGEN_API_SUBMISSION_DELAY').default(5000).asIntPositive();
21
+
22
+ export const graphqlRootPath = env.get('ROOT_PATH').default('/').asString();
23
+
24
+ export const webClientID = env.get('WEB_CLIENT_ID').asString();
25
+ export const tenantID = env.get('TENANT_ID').asString();
26
+
27
+ export const enableIntrospection = env.get('ENABLE_INTROSPECTION').default('false').asBool();
28
+ export const websiteRunFromPackage = env.get('WEBSITE_RUN_FROM_PACKAGE').asIntPositive();
29
+ export const userEmailMap = env.get('USER_EMAIL_MAP').default('{}').asJsonObject() as Record<string, string>;
30
+
31
+ export const ___skipAPIurl = env.get('ASK_SKIP_API_URL').asString();
32
+ export const ___skipAPIOrgId = env.get('ASK_SKIP_ORGANIZATION_ID').asString();
33
+
34
+ export const auth0Domain = env.get('AUTH0_DOMAIN').asString();
35
+ export const auth0WebClientID = env.get('AUTH0_CLIENT_ID').asString();
36
+ export const auth0ClientSecret = env.get('AUTH0_CLIENT_SECRET').asString();
37
+
38
+ export const mj_core_schema = env.get('MJ_CORE_SCHEMA').asString();
39
+
40
+ export const configFile = env.get('CONFIG_FILE').asString();
41
+
42
+ const userHandlingInfoSchema = z.object({
43
+ autoCreateNewUsers: z.boolean().optional().default(false),
44
+ newUserLimitedToAuthorizedDomains: z.boolean().optional().default(false),
45
+ newUserAuthorizedDomains: z.array(z.string()).optional().default([]),
46
+ newUserRoles: z.array(z.string()).optional().default([]),
47
+ updateCacheWhenNotFound: z.boolean().optional().default(false),
48
+ updateCacheWhenNotFoundDelay: z.number().optional().default(30000),
49
+ contextUserForNewUserCreation: z.string().optional().default(''),
50
+ });
51
+
52
+ const databaseSettingsInfoSchema = z.object({
53
+ connectionTimeout: z.number(),
54
+ requestTimeout: z.number(),
55
+ metadataCacheRefreshInterval: z.number(),
56
+ });
57
+
58
+ const viewingSystemInfoSchema = z.object({
59
+ enableSmartFilters: z.boolean().optional(),
60
+ });
61
+
62
+ const askSkipInfoSchema = z.object({
63
+ organizationInfo: z.string().optional(),
64
+ });
65
+
66
+ const configInfoSchema = z.object({
67
+ userHandling: userHandlingInfoSchema,
68
+ databaseSettings: databaseSettingsInfoSchema,
69
+ viewingSystem: viewingSystemInfoSchema.optional(),
70
+ askSkip: askSkipInfoSchema.optional(),
71
+ });
72
+
73
+ export type UserHandlingInfo = z.infer<typeof userHandlingInfoSchema>;
74
+ export type DatabaseSettingsInfo = z.infer<typeof databaseSettingsInfoSchema>;
75
+ export type ViewingSystemSettingsInfo = z.infer<typeof viewingSystemInfoSchema>;
76
+ export type ConfigInfo = z.infer<typeof configInfoSchema>;
77
+
78
+ export const configInfo: ConfigInfo = loadConfig();
79
+
80
+ export function loadConfig() {
81
+ const configPath = configFile ?? path.resolve('config.json');
82
+
83
+ if (!fs.existsSync(configPath)) {
84
+ throw new Error(`Config file ${configPath} does not exist.`);
85
+ }
86
+
87
+ const configData = fs.readFileSync(configPath, 'utf-8');
88
+ return configInfoSchema.parse(JSON.parse(configData));
89
+ }
package/src/context.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { IncomingMessage } from 'http';
2
+ import * as url from 'url';
3
+ import { default as jwt } from 'jsonwebtoken';
4
+ import 'reflect-metadata';
5
+ import { Subject, firstValueFrom } from 'rxjs';
6
+ import { AuthenticationError, AuthorizationError } from 'type-graphql';
7
+ import { DataSource } from 'typeorm';
8
+ import { getSigningKeys, validationOptions, verifyUserRecord } from './auth/index.js';
9
+ import { authCache } from './cache.js';
10
+ import { userEmailMap } from './config.js';
11
+ import { UserPayload } from './types.js';
12
+ import { TokenExpiredError } from './auth/index.js';
13
+
14
+ const verifyAsync = async (issuer: string, options: jwt.VerifyOptions, token: string): Promise<jwt.JwtPayload> =>
15
+ new Promise((resolve, reject) => {
16
+ jwt.verify(token, getSigningKeys(issuer), options, (err, jwt) => {
17
+ if (jwt && typeof jwt !== 'string' && !err) {
18
+ const payload = jwt.payload ?? jwt;
19
+
20
+ console.log(`Valid token: ${payload.name} (${payload.email ? payload.email : payload.preferred_username})`); // temporary fix to check preferred_username if email is not present
21
+ resolve(payload);
22
+ } else {
23
+ console.warn('Invalid token');
24
+ reject(err);
25
+ }
26
+ });
27
+ });
28
+
29
+ export const getUserPayload = async (
30
+ bearerToken: string,
31
+ sessionId = 'default',
32
+ dataSource: DataSource,
33
+ requestDomain?: string
34
+ ): Promise<UserPayload> => {
35
+ try {
36
+ const token = bearerToken.replace('Bearer ', '');
37
+
38
+ if (!token) {
39
+ console.warn('No token to validate');
40
+ throw new AuthenticationError('Missing token');
41
+ }
42
+
43
+ const payload = jwt.decode(token);
44
+ if (!payload || typeof payload === 'string') {
45
+ throw new AuthenticationError('Invalid token payload');
46
+ }
47
+
48
+ const expiryDate = new Date((payload.exp ?? 0) * 1000);
49
+ if (expiryDate.getTime() <= Date.now()) {
50
+ throw new TokenExpiredError(expiryDate);
51
+ }
52
+
53
+ if (!authCache.has(token)) {
54
+ const issuer = payload.iss;
55
+ if (!issuer) {
56
+ console.warn('No issuer claim on token');
57
+ throw new AuthenticationError('Missing issuer claim on token');
58
+ }
59
+
60
+ await verifyAsync(issuer, validationOptions[issuer], token);
61
+ authCache.set(token, true);
62
+ }
63
+
64
+ const email = payload?.email ? userEmailMap[payload?.email] ?? payload?.email : payload?.preferred_username; // temporary fix to check preferred_username if email is not present
65
+ const fullName = payload?.name;
66
+ const firstName = payload?.given_name || fullName?.split(' ')[0];
67
+ const lastName = payload?.family_name || fullName?.split(' ')[1] || fullName?.split(' ')[0];
68
+ const userRecord = await verifyUserRecord(email, firstName, lastName, requestDomain, dataSource);
69
+
70
+ if (!userRecord) {
71
+ console.error(`User ${email} not found`);
72
+ throw new AuthorizationError();
73
+ } else if (!userRecord.IsActive) {
74
+ console.error(`User ${email} found but inactive`);
75
+ throw new AuthorizationError();
76
+ }
77
+
78
+ return { userRecord, email, sessionId };
79
+ } catch (e) {
80
+ console.error(e);
81
+ if (e instanceof TokenExpiredError) {
82
+ throw e;
83
+ } else return {} as UserPayload;
84
+ }
85
+ };
86
+
87
+ export const contextFunction =
88
+ ({ setupComplete$, dataSource }: { setupComplete$: Subject<unknown>; dataSource: DataSource }) =>
89
+ async ({ req }: { req: IncomingMessage }) => {
90
+ await firstValueFrom(setupComplete$); // wait for setup to complete before processing the request
91
+
92
+ const sessionIdRaw = req.headers['x-session-id'];
93
+ const requestDomain = url.parse(req.headers.origin || '');
94
+ const sessionId = sessionIdRaw ? sessionIdRaw.toString() : '';
95
+ const bearerToken = req.headers.authorization ?? '';
96
+
97
+ const userPayload = await getUserPayload(
98
+ bearerToken,
99
+ sessionId,
100
+ dataSource,
101
+ requestDomain?.hostname ? requestDomain.hostname : undefined
102
+ );
103
+
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ const operationName: string | undefined = (req as any).body?.operationName;
106
+ if (operationName !== 'IntrospectionQuery') {
107
+ console.log({ operationName });
108
+ }
109
+
110
+ return { dataSource, userPayload };
111
+ };
@@ -0,0 +1,42 @@
1
+ import { FieldMapper, MapperKind, getDirective, mapSchema } from '@graphql-tools/utils';
2
+ import { GraphQLFieldResolver, defaultFieldResolver } from 'graphql';
3
+ import { AuthorizationError, Directive } from 'type-graphql';
4
+ import { AppContext, DirectiveBuilder } from '../types.js';
5
+
6
+ const DIRECTIVE_NAME = 'Public';
7
+
8
+ export function Public(): PropertyDecorator & MethodDecorator & ClassDecorator;
9
+ export function Public(): PropertyDecorator | MethodDecorator | ClassDecorator {
10
+ return (targetOrPrototype, propertyKey, descriptor) =>
11
+ Directive(`@${DIRECTIVE_NAME}`)(targetOrPrototype, propertyKey, descriptor);
12
+ }
13
+
14
+ export const publicDirective: DirectiveBuilder = {
15
+ typeDefs: `directive @${DIRECTIVE_NAME} on FIELD_DEFINITION`,
16
+ transformer: (schema) => {
17
+ const fieldMapper: FieldMapper = (fieldConfig) => {
18
+ const directive = getDirective(schema, fieldConfig, DIRECTIVE_NAME)?.[0];
19
+ if (directive) {
20
+ return fieldConfig;
21
+ } else {
22
+ // `@Public` directive not present, so will require auth
23
+ const { resolve = defaultFieldResolver } = fieldConfig;
24
+ const directiveResolver: GraphQLFieldResolver<unknown, AppContext> = async (
25
+ source,
26
+ args,
27
+ context,
28
+ info
29
+ ) => {
30
+ // eslint-disable-next-line
31
+ if (!context?.userPayload?.userRecord?.IsActive) {
32
+ throw new AuthorizationError();
33
+ }
34
+ return await resolve(source, args, context, info);
35
+ };
36
+
37
+ return { ...fieldConfig, resolve: directiveResolver };
38
+ }
39
+ };
40
+ return mapSchema(schema, { [MapperKind.OBJECT_FIELD]: fieldMapper });
41
+ },
42
+ };
@@ -0,0 +1 @@
1
+ export * from './Public.js';
@@ -0,0 +1,29 @@
1
+ import { BaseEntity, PotentialDuplicateRequest } from "@memberjunction/core";
2
+ import { RegisterClass } from "@memberjunction/global";
3
+ import { DuplicateRunEntity } from "@memberjunction/core-entities";
4
+ import { DuplicateRecordDetector } from "@memberjunction/ai-vector-dupe";
5
+
6
+ @RegisterClass(BaseEntity, 'Duplicate Runs', 3)
7
+ export class DuplicateRunEntity_Server extends DuplicateRunEntity {
8
+ public async Save(): Promise<boolean> {
9
+ const saveResult: boolean = await super.Save();
10
+ if (saveResult && this.EndedAt === null) {
11
+ // do something
12
+ const duplicateRecordDetector: DuplicateRecordDetector = new DuplicateRecordDetector();
13
+ let request: PotentialDuplicateRequest = new PotentialDuplicateRequest();
14
+ request.EntityID = this.EntityID;
15
+ request.ListID = this.SourceListID;
16
+ request.Options = {
17
+ DuplicateRunID: this.ID,
18
+ };
19
+
20
+ const response = await duplicateRecordDetector.getDuplicateRecords(request, this.ContextCurrentUser);
21
+ }
22
+
23
+ return saveResult;
24
+ }
25
+ }
26
+
27
+ export function LoadDuplicateRunEntityServerSubClass() {
28
+
29
+ }
@@ -0,0 +1,104 @@
1
+ import { RegisterClass } from '@memberjunction/global';
2
+ import { BaseEntity, EntitySaveOptions } from '@memberjunction/core';
3
+ import { EntityPermissionEntity } from '@memberjunction/core-entities';
4
+ import axios from 'axios';
5
+ import { ___codeGenAPIPort, ___codeGenAPISubmissionDelay, ___codeGenAPIURL } from '../config.js';
6
+
7
+ /**
8
+ * Server-side only class that extends the entity permissions object to watch for changes to entity permissions, build a queue of entities that have been changed, and then from time to time, submit
9
+ * them to an API server that will execute the underlying permission changes at the database level.
10
+ */
11
+ @RegisterClass(BaseEntity, 'Entity Permissions', 3)
12
+ export class EntityPermissionsEntity_Server extends EntityPermissionEntity {
13
+ protected static _entityIDQueue: string[] = [];
14
+ protected static _lastModifiedTime: Date | null = null;
15
+ protected static _submissionTimer: NodeJS.Timeout | null = null;
16
+ protected static _submissionDelay: number = ___codeGenAPISubmissionDelay;
17
+ protected static _baseURL: string = ___codeGenAPIURL;
18
+ protected static _port: number = ___codeGenAPIPort;
19
+ protected static _apiEndpoint: string = '/api/entity-permissions';
20
+
21
+ // Method to construct the full URL dynamically
22
+ protected static getSubmissionURL(): string {
23
+ return `${this._baseURL}:${this._port}${this._apiEndpoint}`;
24
+ }
25
+
26
+ public static get EntityIDQueue(): string[] {
27
+ return this._entityIDQueue;
28
+ }
29
+
30
+ public static ClearQueue(): void {
31
+ this._entityIDQueue = [];
32
+ this._submissionTimer = null;
33
+ }
34
+ public static AddToQueue(entityID: string): void {
35
+ if (this._entityIDQueue.indexOf(entityID) === -1) this._entityIDQueue.push(entityID);
36
+ this._lastModifiedTime = new Date();
37
+ this.CheckStartSubmissionTimer();
38
+ }
39
+
40
+ protected static CheckStartSubmissionTimer(): void {
41
+ if (this._submissionTimer === null) {
42
+ this.StartSubmissionTimer();
43
+ } else {
44
+ // we need to cancel the existing timer and start a new one
45
+ clearTimeout(this._submissionTimer);
46
+ this.StartSubmissionTimer();
47
+ }
48
+ }
49
+
50
+ protected static StartSubmissionTimer(): void {
51
+ this._submissionTimer = setTimeout(() => {
52
+ this.SubmitQueue();
53
+ }, this._submissionDelay);
54
+ }
55
+
56
+ protected static async SubmitQueue(): Promise<void> {
57
+ this._lastModifiedTime = null;
58
+
59
+ // now, use Axios to submit the queue to the API server
60
+ // Check if there's anything to submit
61
+ if (this._entityIDQueue.length > 0) {
62
+ try {
63
+ // Use Axios to submit the queue to the API server
64
+ const response = await axios.post(this.getSubmissionURL(), {
65
+ entityIDArray: this._entityIDQueue,
66
+ });
67
+
68
+ // Check the Axios response code implicitly and API response explicitly
69
+ if (response.status === 200 && response.data.status === 'ok') {
70
+ console.log('Queue submitted successfully.');
71
+ // now, clear the queue and timer
72
+ this.ClearQueue();
73
+ } else {
74
+ // Handle API indicating a failure
75
+ console.error('Failed to submit queue:', response.data.errorMessage || 'Unknown error');
76
+ }
77
+ } catch (error) {
78
+ // Handle errors here
79
+ console.error('Failed to submit queue:', error);
80
+ // Consider re-trying or logging the error based on your requirements
81
+ }
82
+ } else {
83
+ console.log('No entities to submit.');
84
+ }
85
+ }
86
+
87
+ override Save(options?: EntitySaveOptions): Promise<boolean> {
88
+ // simply queue up the entity ID
89
+ if (this.Dirty || options?.IgnoreDirtyState) EntityPermissionsEntity_Server.AddToQueue(this.EntityID);
90
+
91
+ return super.Save(options);
92
+ }
93
+
94
+ override async Delete(): Promise<boolean> {
95
+ const success = await super.Delete();
96
+
97
+ // simply queue up the entity ID if the delete worked
98
+ if (success) EntityPermissionsEntity_Server.AddToQueue(this.EntityID);
99
+
100
+ return success;
101
+ }
102
+ }
103
+
104
+ export function LoadEntityPermissionsServerSubClass() {}