@memberjunction/server 6.1.0-edge.1 → 6.1.0-edge.3
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/LICENSE +180 -4
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +65 -18
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +1 -1
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.js +1 -1
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +540 -1595
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2755 -16857
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +58 -21
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -54
- package/dist/index.js.map +1 -1
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +50 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +9 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -1
- package/dist/logging/secretRedactor.js +39 -0
- package/dist/logging/secretRedactor.js.map +1 -1
- package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
- package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
- package/dist/middleware/BaseServerMiddleware.js +5 -0
- package/dist/middleware/BaseServerMiddleware.js.map +1 -1
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.js +1 -1
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +12 -0
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -1
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +85 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +302 -82
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js +1 -1
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +130 -44
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/SignatureWebhookHandler.js +1 -1
- package/dist/rest/SignatureWebhookHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
- package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
- package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/ScheduledJobsService.js +1 -1
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +37 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +34 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphActionRunner.js +95 -0
- package/dist/services/TaskGraphActionRunner.js.map +1 -0
- package/dist/services/TaskGraphAgentRunner.d.ts +20 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +53 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
- package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
- package/dist/services/TaskGraphPromptRunner.js +134 -0
- package/dist/services/TaskGraphPromptRunner.js.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +1 -1
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js +1 -1
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
- package/dist/telephony/VonageTelephonyRouter.js +1 -1
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +91 -91
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
- package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
- package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/SessionManager.test.ts +1 -1
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/WidgetSessionService.test.ts +1 -1
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/databaseAbstraction.test.ts +1 -1
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/resolverBase.rls.test.ts +3 -0
- package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/__tests__/widgetGuestElevation.test.ts +1 -1
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/auth/index.ts +69 -21
- package/src/auth/initializeProviders.ts +128 -7
- package/src/auth/magicLink/MagicLinkService.ts +1 -1
- package/src/auth/newUsers.ts +1 -1
- package/src/config.ts +55 -36
- package/src/context.ts +2 -2
- package/src/generated/generated.ts +2038 -10529
- package/src/generic/ResolverBase.ts +63 -21
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +199 -56
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/logging/secretRedactor.ts +43 -0
- package/src/middleware/BaseServerMiddleware.ts +5 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/realtimeWidget/WidgetSessionService.ts +1 -1
- package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
- package/src/resolvers/ActionResolver.ts +13 -0
- package/src/resolvers/ComponentRegistryResolver.ts +1 -1
- package/src/resolvers/EntityPermissionResolver.ts +1 -1
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +328 -70
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/SearchEntitiesResolver.ts +1 -1
- package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserFavoriteResolver.ts +1 -1
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +150 -42
- package/src/rest/SignatureWebhookHandler.ts +1 -1
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/ScheduledJobsService.ts +1 -1
- package/src/services/StartTaskGraphDispatcher.ts +38 -1
- package/src/services/TaskGraphActionRunner.ts +99 -0
- package/src/services/TaskGraphAgentRunner.ts +58 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- package/src/services/TaskGraphPromptRunner.ts +144 -0
- package/src/telephony/RingCentralTelephonyService.ts +1 -1
- package/src/telephony/TwilioTelephonyRouter.ts +1 -1
- package/src/telephony/VonageTelephonyRouter.ts +1 -1
- package/src/types.ts +6 -0
|
@@ -1,54 +1,46 @@
|
|
|
1
|
-
import { AppContext, Arg, Ctx,
|
|
1
|
+
import { AppContext, Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from '@memberjunction/server';
|
|
2
|
+
import { IMetadataProvider, RunView, UserInfo } from '@memberjunction/core';
|
|
2
3
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
3
4
|
import { MJUser_, MJUserRole_, MJUserResolverBase } from '../generated/generated.js';
|
|
4
5
|
import { GetReadOnlyProvider } from '../util.js';
|
|
6
|
+
import {
|
|
7
|
+
AssertRoleLoadSucceeded,
|
|
8
|
+
RequireContextUser,
|
|
9
|
+
RequireLoadedUser,
|
|
10
|
+
RequireRoleProvider,
|
|
11
|
+
UserIdExtraFilter,
|
|
12
|
+
} from './currentUserRoles.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* CurrentUser payload. Extends the generated user type with a first-class `Roles`
|
|
16
|
+
* field — child-array FieldResolvers (`MJUserRoles_UserIDArray`) are no longer
|
|
17
|
+
* generated. Roles come from the request's `UserInfo` (so magic-link anonymous
|
|
18
|
+
* sessions keep their synthesized roles) or from a RunView on `MJ: User Roles`.
|
|
19
|
+
*/
|
|
20
|
+
@ObjectType()
|
|
21
|
+
export class CurrentUserType extends MJUser_ {
|
|
22
|
+
@Field(() => [MJUserRole_])
|
|
23
|
+
Roles: MJUserRole_[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type RoleSource = {
|
|
27
|
+
ID?: string;
|
|
28
|
+
UserID: string;
|
|
29
|
+
RoleID: string;
|
|
30
|
+
RoleName?: string;
|
|
31
|
+
Role?: string;
|
|
32
|
+
__mj_CreatedAt?: Date;
|
|
33
|
+
__mj_UpdatedAt?: Date;
|
|
34
|
+
};
|
|
5
35
|
|
|
6
36
|
@Resolver(MJUser_)
|
|
7
37
|
export class UserResolver extends MJUserResolverBase {
|
|
8
38
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
* Overrides the generated field resolver to support ANONYMOUS magic-link sessions: those
|
|
13
|
-
* resolve to a shared Anonymous principal that holds NO roles in the database (by design —
|
|
14
|
-
* persisting them would let anonymous sessions accrete privileges across links). The roles
|
|
15
|
-
* are synthesized per-request in {@link buildMagicLinkSessionUser} and live only on the
|
|
16
|
-
* request's `UserInfo`. For the session's OWN user row we serve those synthesized roles
|
|
17
|
-
* directly, skipping the generated DB query (which returns empty) and its `MJ: User Roles`
|
|
18
|
-
* read-permission check (which the restricted anon role wouldn't pass). Every other case —
|
|
19
|
-
* named users, named magic-link users, listing other users' roles — falls through to the
|
|
20
|
-
* generated resolver unchanged, so there is no behavioral or security change off this path.
|
|
21
|
-
*/
|
|
22
|
-
@FieldResolver(() => [MJUserRole_])
|
|
23
|
-
async MJUserRoles_UserIDArray(@Root() mjuser_: MJUser_, @Ctx() context: AppContext, @PubSub() pubSub: PubSubEngine) {
|
|
24
|
-
const sessionUser = context.userPayload?.userRecord;
|
|
25
|
-
if (sessionUser?.IsMagicLinkAnonymous && UUIDsEqual(mjuser_.ID, sessionUser.ID)) {
|
|
26
|
-
const now = new Date();
|
|
27
|
-
// Build rows with the entity's RAW field names, then run them through the same
|
|
28
|
-
// name→code-name mapping the generated resolver uses (e.g. __mj_CreatedAt →
|
|
29
|
-
// _mj__CreatedAt). Returning raw names directly trips GraphQL's non-nullable check.
|
|
30
|
-
const rows = (sessionUser.UserRoles ?? []).map((r: { UserID: string; RoleID: string; RoleName?: string; Role?: string }) => ({
|
|
31
|
-
ID: sessionUser.ID,
|
|
32
|
-
UserID: r.UserID,
|
|
33
|
-
RoleID: r.RoleID,
|
|
34
|
-
User: sessionUser.Name,
|
|
35
|
-
Role: r.RoleName ?? r.Role,
|
|
36
|
-
__mj_CreatedAt: now,
|
|
37
|
-
__mj_UpdatedAt: now,
|
|
38
|
-
}));
|
|
39
|
-
return this.ArrayMapFieldNamesToCodeNames('MJ: User Roles', rows, sessionUser);
|
|
40
|
-
}
|
|
41
|
-
return super.MJUserRoles_UserIDArray(mjuser_, context, pubSub);
|
|
42
|
-
}
|
|
43
|
-
@Query(() => MJUser_)
|
|
44
|
-
async CurrentUser(@Ctx() context: AppContext) {
|
|
45
|
-
// Check API key scope authorization for user read (self)
|
|
39
|
+
@Query(() => CurrentUserType)
|
|
40
|
+
async CurrentUser(@Ctx() context: AppContext): Promise<CurrentUserType> {
|
|
46
41
|
await this.CheckAPIKeyScopeAuthorization('user:read', '*', context.userPayload);
|
|
47
42
|
|
|
48
|
-
// If userRecord is already available (e.g., from API key auth or system auth),
|
|
49
|
-
// use it directly instead of looking up by email again
|
|
50
43
|
if (context.userPayload.userRecord) {
|
|
51
|
-
// Convert UserInfo to the GraphQL response format
|
|
52
44
|
const userRecord = context.userPayload.userRecord;
|
|
53
45
|
const userData = {
|
|
54
46
|
ID: userRecord.ID,
|
|
@@ -65,30 +57,29 @@ export class UserResolver extends MJUserResolverBase {
|
|
|
65
57
|
LinkedEntityRecordID: userRecord.LinkedEntityRecordID,
|
|
66
58
|
__mj_CreatedAt: userRecord.__mj_CreatedAt,
|
|
67
59
|
__mj_UpdatedAt: userRecord.__mj_UpdatedAt,
|
|
68
|
-
// Include the UserRoles sub-array for the GraphQL response
|
|
69
|
-
MJUserRoles_UserIDArray: userRecord.UserRoles?.map((r: { ID: string; UserID: string; RoleID: string; RoleName?: string; __mj_CreatedAt?: Date; __mj_UpdatedAt?: Date }) => ({
|
|
70
|
-
ID: r.ID,
|
|
71
|
-
UserID: r.UserID,
|
|
72
|
-
RoleID: r.RoleID,
|
|
73
|
-
User: userRecord.Name,
|
|
74
|
-
Role: r.RoleName,
|
|
75
|
-
__mj_CreatedAt: r.__mj_CreatedAt,
|
|
76
|
-
__mj_UpdatedAt: r.__mj_UpdatedAt,
|
|
77
|
-
})) || [],
|
|
78
60
|
};
|
|
79
61
|
console.log('CurrentUser (from userRecord)', userData.Email);
|
|
80
|
-
|
|
62
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
63
|
+
const mapped = await this.MapFieldNamesToCodeNames('MJ: Users', userData, userRecord, provider) as MJUser_;
|
|
64
|
+
return {
|
|
65
|
+
...mapped,
|
|
66
|
+
Roles: await this.mapRolesFromUserInfo(userRecord.UserRoles ?? [], userRecord, userRecord, provider),
|
|
67
|
+
};
|
|
81
68
|
}
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
70
|
+
const result = RequireLoadedUser(
|
|
71
|
+
await this.UserByEmail(context.userPayload.email, context),
|
|
72
|
+
context.userPayload.email,
|
|
73
|
+
);
|
|
74
|
+
console.log('CurrentUser (from email lookup)', result.Email);
|
|
75
|
+
return {
|
|
76
|
+
...result,
|
|
77
|
+
Roles: await this.loadRolesForUser(result, context),
|
|
78
|
+
};
|
|
87
79
|
}
|
|
88
80
|
|
|
89
81
|
@Query(() => MJUser_)
|
|
90
82
|
async UserByID(@Arg('ID', () => Int) ID: number, @Ctx() { providers, userPayload }: AppContext) {
|
|
91
|
-
// Check API key scope authorization for user read
|
|
92
83
|
await this.CheckAPIKeyScopeAuthorization('user:read', ID.toString(), userPayload);
|
|
93
84
|
|
|
94
85
|
const provider = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true})
|
|
@@ -98,7 +89,6 @@ export class UserResolver extends MJUserResolverBase {
|
|
|
98
89
|
|
|
99
90
|
@Query(() => MJUser_)
|
|
100
91
|
async UserByEmployeeID(@Arg('EmployeeID', () => Int) EmployeeID: number, @Ctx() { providers, userPayload }: AppContext) {
|
|
101
|
-
// Check API key scope authorization for user read
|
|
102
92
|
await this.CheckAPIKeyScopeAuthorization('user:read', EmployeeID.toString(), userPayload);
|
|
103
93
|
|
|
104
94
|
const provider = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true})
|
|
@@ -108,14 +98,63 @@ export class UserResolver extends MJUserResolverBase {
|
|
|
108
98
|
|
|
109
99
|
@Query(() => MJUser_)
|
|
110
100
|
async UserByEmail(@Arg('Email', () => String) Email: string, @Ctx() { providers, userPayload }: AppContext) {
|
|
111
|
-
// Check API key scope authorization for user read
|
|
112
101
|
await this.CheckAPIKeyScopeAuthorization('user:read', Email, userPayload);
|
|
113
102
|
|
|
114
|
-
// const searchEmail = userEmailMap[Email] ?? Email;
|
|
115
103
|
const searchEmail = Email;
|
|
116
104
|
const provider = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true})
|
|
117
105
|
const returnVal = super.safeFirstArrayElement(await this.findBy(provider, 'MJ: Users', { Email: searchEmail }, userPayload.userRecord));
|
|
118
106
|
return this.MapFieldNamesToCodeNames('MJ: Users', returnVal);
|
|
119
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Magic-link anonymous sessions hold synthesized roles on UserInfo that are
|
|
111
|
+
* not persisted. Always prefer those when the requested row is the session
|
|
112
|
+
* user. Everyone else loads `MJ: User Roles` via RunView.
|
|
113
|
+
*/
|
|
114
|
+
private async loadRolesForUser(user: MJUser_, context: AppContext): Promise<MJUserRole_[]> {
|
|
115
|
+
const contextUser = RequireContextUser(this.GetUserFromPayload(context.userPayload));
|
|
116
|
+
|
|
117
|
+
if (contextUser.IsMagicLinkAnonymous && UUIDsEqual(user.ID, contextUser.ID)) {
|
|
118
|
+
const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
|
|
119
|
+
return await this.mapRolesFromUserInfo(contextUser.UserRoles ?? [], contextUser, contextUser, provider);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const provider = RequireRoleProvider(
|
|
123
|
+
GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true }),
|
|
124
|
+
);
|
|
125
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
126
|
+
const result = await rv.RunView<RoleSource>({
|
|
127
|
+
EntityName: 'MJ: User Roles',
|
|
128
|
+
ExtraFilter: UserIdExtraFilter(user.ID),
|
|
129
|
+
ResultType: 'simple',
|
|
130
|
+
}, contextUser);
|
|
131
|
+
|
|
132
|
+
AssertRoleLoadSucceeded(result, user.ID);
|
|
133
|
+
return await this.ArrayMapFieldNamesToCodeNames(
|
|
134
|
+
'MJ: User Roles',
|
|
135
|
+
result.Results ?? [],
|
|
136
|
+
contextUser,
|
|
137
|
+
provider,
|
|
138
|
+
) as MJUserRole_[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async mapRolesFromUserInfo(
|
|
142
|
+
roles: RoleSource[],
|
|
143
|
+
user: { ID: string; Name?: string },
|
|
144
|
+
contextUser?: UserInfo,
|
|
145
|
+
provider?: IMetadataProvider,
|
|
146
|
+
): Promise<MJUserRole_[]> {
|
|
147
|
+
const now = new Date();
|
|
148
|
+
const rows = roles.map((r) => ({
|
|
149
|
+
ID: r.ID ?? user.ID,
|
|
150
|
+
UserID: r.UserID,
|
|
151
|
+
RoleID: r.RoleID,
|
|
152
|
+
User: user.Name,
|
|
153
|
+
Role: r.RoleName ?? r.Role,
|
|
154
|
+
__mj_CreatedAt: r.__mj_CreatedAt ?? now,
|
|
155
|
+
__mj_UpdatedAt: r.__mj_UpdatedAt ?? now,
|
|
156
|
+
}));
|
|
157
|
+
return await this.ArrayMapFieldNamesToCodeNames('MJ: User Roles', rows, contextUser, provider) as MJUserRole_[];
|
|
158
|
+
}
|
|
120
159
|
}
|
|
121
160
|
export default UserResolver;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { LogError } from '@memberjunction/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Guards and filter builders for CurrentUser role loading.
|
|
5
|
+
* Kept out of UserResolver so the login-adjacent failure modes are unit-testable
|
|
6
|
+
* without importing generated GraphQL types or the Apollo config bootstrap.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function RequireLoadedUser<T extends { ID?: string }>(
|
|
10
|
+
user: T | null | undefined,
|
|
11
|
+
email: string | undefined,
|
|
12
|
+
): T & { ID: string } {
|
|
13
|
+
if (!user?.ID) {
|
|
14
|
+
throw new Error(`CurrentUser: no user found for email '${email ?? ''}'`);
|
|
15
|
+
}
|
|
16
|
+
return user as T & { ID: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function RequireContextUser<T>(user: T | null | undefined): T {
|
|
20
|
+
if (!user) {
|
|
21
|
+
throw new Error('CurrentUser: authenticated context user is required to load roles');
|
|
22
|
+
}
|
|
23
|
+
return user;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function RequireRoleProvider<T>(provider: T | null | undefined): T {
|
|
27
|
+
if (!provider) {
|
|
28
|
+
throw new Error('CurrentUser: no data provider available to load roles');
|
|
29
|
+
}
|
|
30
|
+
return provider;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Quote-escape a server-resolved user id for an ExtraFilter literal. */
|
|
34
|
+
export function UserIdExtraFilter(userId: string): string {
|
|
35
|
+
return `UserID='${userId.replace(/'/g, "''")}'`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function AssertRoleLoadSucceeded(
|
|
39
|
+
result: { Success: boolean; ErrorMessage?: string | null },
|
|
40
|
+
userId: string,
|
|
41
|
+
): void {
|
|
42
|
+
if (!result.Success) {
|
|
43
|
+
const message = result.ErrorMessage ?? 'unknown error';
|
|
44
|
+
LogError(`CurrentUser: failed to load roles for user ${userId}: ${message}`);
|
|
45
|
+
throw new Error(`CurrentUser: failed to load roles: ${message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -24,6 +24,9 @@ import jwt from 'jsonwebtoken';
|
|
|
24
24
|
/** The token's `typ` claim — the route rejects any token whose `typ` differs. */
|
|
25
25
|
export const MEDIA_ACCESS_TOKEN_TYPE = 'media-access';
|
|
26
26
|
|
|
27
|
+
/** The upload staging token's `typ` claim. */
|
|
28
|
+
export const MEDIA_UPLOAD_ACCESS_TOKEN_TYPE = 'media-upload';
|
|
29
|
+
|
|
27
30
|
/** Default media-access token lifetime (hours). Long enough to cover a full playback session. */
|
|
28
31
|
export const MEDIA_ACCESS_DEFAULT_TTL_HOURS = 4;
|
|
29
32
|
|
|
@@ -64,7 +67,13 @@ export class MediaAccessKeyManager extends BaseSingleton<MediaAccessKeyManager>
|
|
|
64
67
|
|
|
65
68
|
/** Resolves the signing secret once, preferring the deployment's symmetric key. */
|
|
66
69
|
private ensureSecret(): void {
|
|
67
|
-
if (this._initialized) {
|
|
70
|
+
if (this._initialized && this.secret) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const store = this.GetGlobalObjectStore();
|
|
74
|
+
if (store && typeof store['___MEDIA_ACCESS_SECRET___'] === 'string') {
|
|
75
|
+
this.secret = store['___MEDIA_ACCESS_SECRET___'];
|
|
76
|
+
this._initialized = true;
|
|
68
77
|
return;
|
|
69
78
|
}
|
|
70
79
|
const configured = process.env.MJ_BASE_ENCRYPTION_KEY?.trim();
|
|
@@ -78,6 +87,9 @@ export class MediaAccessKeyManager extends BaseSingleton<MediaAccessKeyManager>
|
|
|
78
87
|
'MJ_BASE_ENCRYPTION_KEY for stable signing.',
|
|
79
88
|
);
|
|
80
89
|
}
|
|
90
|
+
if (store) {
|
|
91
|
+
store['___MEDIA_ACCESS_SECRET___'] = this.secret;
|
|
92
|
+
}
|
|
81
93
|
this._initialized = true;
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -95,6 +107,39 @@ export class MediaAccessKeyManager extends BaseSingleton<MediaAccessKeyManager>
|
|
|
95
107
|
return { Token: token, ExpiresAt: expiresAt };
|
|
96
108
|
}
|
|
97
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Mints a signed upload staging token for a user, expiring after `ttlMinutes` (default 10 minutes).
|
|
112
|
+
*/
|
|
113
|
+
public SignUpload(userId: string, ttlMinutes: number = 10): { Token: string; ExpiresAt: Date } {
|
|
114
|
+
this.ensureSecret();
|
|
115
|
+
const claims = { fileId: '*', userId, typ: MEDIA_UPLOAD_ACCESS_TOKEN_TYPE };
|
|
116
|
+
const expiresInSeconds = Math.floor(ttlMinutes * 60);
|
|
117
|
+
const token = jwt.sign(claims, this.secret, { algorithm: 'HS256', expiresIn: expiresInSeconds });
|
|
118
|
+
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
|
|
119
|
+
return { Token: token, ExpiresAt: expiresAt };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Verifies an upload staging token.
|
|
124
|
+
*/
|
|
125
|
+
public VerifyUpload(token: string): { Valid: boolean; UserId?: string; Error?: string } {
|
|
126
|
+
this.ensureSecret();
|
|
127
|
+
try {
|
|
128
|
+
const decoded = jwt.verify(token, this.secret, { algorithms: ['HS256'] });
|
|
129
|
+
if (
|
|
130
|
+
typeof decoded === 'object' && decoded !== null &&
|
|
131
|
+
(decoded as { typ?: unknown }).typ === MEDIA_UPLOAD_ACCESS_TOKEN_TYPE &&
|
|
132
|
+
typeof (decoded as { userId?: unknown }).userId === 'string'
|
|
133
|
+
) {
|
|
134
|
+
return { Valid: true, UserId: (decoded as { userId: string }).userId };
|
|
135
|
+
}
|
|
136
|
+
return { Valid: false, Error: 'Invalid token payload format or claim mismatch' };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
139
|
+
return { Valid: false, Error: msg };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
98
143
|
/**
|
|
99
144
|
* Verifies a token's signature, expiry, and `typ`. Returns the typed claims on
|
|
100
145
|
* success. Never throws — an invalid/expired/malformed token returns `{ Valid: false }`.
|
|
@@ -119,3 +164,4 @@ export class MediaAccessKeyManager extends BaseSingleton<MediaAccessKeyManager>
|
|
|
119
164
|
}
|
|
120
165
|
}
|
|
121
166
|
}
|
|
167
|
+
|
|
@@ -25,6 +25,7 @@ import { FileStorageEngine } from '@memberjunction/storage';
|
|
|
25
25
|
import type { FileStorageBase, ByteRange } from '@memberjunction/storage';
|
|
26
26
|
import { getSystemUser } from '../auth/index.js';
|
|
27
27
|
import { MediaAccessKeyManager } from './MediaAccessKeys.js';
|
|
28
|
+
import { UploadTokenManager } from './UploadTokenManager.js';
|
|
28
29
|
import { parseRange, parseRangeHeaderLoose } from './mediaRange.js';
|
|
29
30
|
|
|
30
31
|
/** A located bytes source for a file: the driver + the provider key to read. */
|
|
@@ -32,21 +33,110 @@ interface FileBytesSource {
|
|
|
32
33
|
driver: FileStorageBase;
|
|
33
34
|
providerKey: string;
|
|
34
35
|
contentType: string;
|
|
36
|
+
fileName: string;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
38
39
|
/**
|
|
39
|
-
* Builds the Express router exposing
|
|
40
|
-
* and
|
|
40
|
+
* Builds the Express router exposing:
|
|
41
|
+
* - `GET /media/:fileId` and `GET /media/:fileId/:filename` (Range streaming)
|
|
42
|
+
* - `POST /media/upload-stage` (Raw binary upload staging)
|
|
41
43
|
*/
|
|
42
44
|
export function createMediaStreamRouter(): Router {
|
|
43
45
|
const router = express.Router();
|
|
46
|
+
|
|
47
|
+
router.post(
|
|
48
|
+
'/upload-stage',
|
|
49
|
+
express.raw({ type: () => true, limit: '100mb' }),
|
|
50
|
+
async (req: Request, res: Response) => {
|
|
51
|
+
await handleUploadStageRequest(req, res);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
44
55
|
router.get('/:fileId', async (req: Request, res: Response) => {
|
|
45
56
|
await handleMediaRequest(req, res);
|
|
46
57
|
});
|
|
58
|
+
router.get('/:fileId/:filename', async (req: Request, res: Response) => {
|
|
59
|
+
await handleMediaRequest(req, res);
|
|
60
|
+
});
|
|
47
61
|
return router;
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Authenticated raw binary upload staging handler (`POST /media/upload-stage`).
|
|
66
|
+
*
|
|
67
|
+
* Accepts raw file binary bytes directly in request body, stages in UploadTokenManager
|
|
68
|
+
* memory cache, and returns an ephemeral single-use upload token for the GraphQL mutation.
|
|
69
|
+
*
|
|
70
|
+
* Security: Requires a cryptographically signed media-upload token minted by `CreateUploadStageToken`
|
|
71
|
+
* (verified via `MediaAccessKeyManager.Instance.VerifyUpload`).
|
|
72
|
+
*/
|
|
73
|
+
async function handleUploadStageRequest(req: Request, res: Response): Promise<void> {
|
|
74
|
+
// Never let CDNs or shared caches retain upload endpoints
|
|
75
|
+
res.setHeader('Cache-Control', 'private, no-store');
|
|
76
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
77
|
+
|
|
78
|
+
// 1. Resolve signed upload token strictly from Authorization header (avoids bearer token in URL logs)
|
|
79
|
+
const authHeader = req.headers.authorization || '';
|
|
80
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.substring(7).trim() : authHeader.trim();
|
|
81
|
+
|
|
82
|
+
if (!token) {
|
|
83
|
+
console.warn('[MediaStreamHandler] POST /media/upload-stage: Missing or empty Authorization header');
|
|
84
|
+
res.status(401).json({ Success: false, ErrorMessage: 'Authorization header with Bearer token required.' });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Cryptographically verify token signature, expiry, and 'media-upload' typ claim
|
|
89
|
+
const uploadVerify = MediaAccessKeyManager.Instance.VerifyUpload(token);
|
|
90
|
+
if (!uploadVerify.Valid || !uploadVerify.UserId) {
|
|
91
|
+
console.warn(`[MediaStreamHandler] POST /media/upload-stage: Token verification failed: ${uploadVerify.Error || 'invalid or expired'}`);
|
|
92
|
+
res.status(401).json({ Success: false, ErrorMessage: `Invalid or expired upload token: ${uploadVerify.Error || 'unauthorized'}` });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const userId = uploadVerify.UserId;
|
|
97
|
+
|
|
98
|
+
// 2. Validate binary body
|
|
99
|
+
const buffer = req.body as Buffer;
|
|
100
|
+
if (!buffer || !Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
101
|
+
res.status(400).json({ Success: false, ErrorMessage: 'Empty or invalid file payload.' });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 3. Extract and sanitize metadata
|
|
106
|
+
const rawFileName = (req.headers['x-file-name'] as string) || (req.query.fileName as string) || 'upload.bin';
|
|
107
|
+
let fileName = 'upload.bin';
|
|
108
|
+
try {
|
|
109
|
+
fileName = decodeURIComponent(rawFileName);
|
|
110
|
+
} catch {
|
|
111
|
+
fileName = rawFileName;
|
|
112
|
+
}
|
|
113
|
+
// Sanitize filename: remove directory traversal, leading slashes, control chars
|
|
114
|
+
fileName = fileName.replace(/[/\\]+/g, '_').replace(/^\.+/, '').replace(/[\x00-\x1f\x7f]/g, '').trim() || 'upload.bin';
|
|
115
|
+
|
|
116
|
+
const mimeType = (req.headers['content-type'] as string) || (req.query.mimeType as string) || 'application/octet-stream';
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const uploadToken = UploadTokenManager.Instance.Stage({
|
|
120
|
+
buffer,
|
|
121
|
+
fileName,
|
|
122
|
+
mimeType,
|
|
123
|
+
userId,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
res.status(200).json({
|
|
127
|
+
Success: true,
|
|
128
|
+
UploadToken: uploadToken,
|
|
129
|
+
FileName: fileName,
|
|
130
|
+
MimeType: mimeType,
|
|
131
|
+
ContentLength: buffer.length,
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
135
|
+
LogError(`[MediaStream] Upload staging failed for '${fileName}': ${message}`);
|
|
136
|
+
res.status(400).json({ Success: false, ErrorMessage: message });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
50
140
|
/** Top-level request handler: verify token → resolve bytes → stream/buffer with Range support. */
|
|
51
141
|
async function handleMediaRequest(req: Request, res: Response): Promise<void> {
|
|
52
142
|
// Express type defs allow req.params values to be string | string[]; normalize to string
|
|
@@ -136,6 +226,7 @@ async function resolveFileBytesSource(fileId: string): Promise<FileBytesSource |
|
|
|
136
226
|
driver,
|
|
137
227
|
providerKey: file.ProviderKey,
|
|
138
228
|
contentType: file.ContentType ?? 'application/octet-stream',
|
|
229
|
+
fileName: file.Name || 'file',
|
|
139
230
|
};
|
|
140
231
|
}
|
|
141
232
|
|
|
@@ -166,6 +257,8 @@ async function serveViaStream(res: Response, source: FileBytesSource, rangeHeade
|
|
|
166
257
|
|
|
167
258
|
res.setHeader('Accept-Ranges', 'bytes');
|
|
168
259
|
res.setHeader('Content-Type', result.ContentType ?? source.contentType);
|
|
260
|
+
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(source.fileName)}"`);
|
|
261
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
169
262
|
if (result.ContentLength != null) {
|
|
170
263
|
res.setHeader('Content-Length', String(result.ContentLength));
|
|
171
264
|
}
|
|
@@ -198,6 +291,8 @@ async function serveViaBuffer(res: Response, source: FileBytesSource, rangeHeade
|
|
|
198
291
|
|
|
199
292
|
res.setHeader('Accept-Ranges', 'bytes');
|
|
200
293
|
res.setHeader('Content-Type', source.contentType);
|
|
294
|
+
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(source.fileName)}"`);
|
|
295
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
201
296
|
|
|
202
297
|
if (!rangeHeader) {
|
|
203
298
|
res.status(200);
|