@commercetools/checkout-payments-processor-sdk 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +42 -0
- package/biome.json +38 -0
- package/package.json +48 -0
- package/src/api/context/request-context.helper.test.ts +98 -0
- package/src/api/context/request-context.helper.ts +89 -0
- package/src/api/context/request-context.provider.ts +27 -0
- package/src/api/context/types/request-context.type.ts +18 -0
- package/src/api/handlers/payment-components.handler.test.ts +378 -0
- package/src/api/handlers/payment-components.handler.ts +80 -0
- package/src/api/handlers/payment-intents.handler.test.ts +475 -0
- package/src/api/handlers/payment-intents.handler.ts +169 -0
- package/src/api/handlers/status.handler.test.ts +551 -0
- package/src/api/handlers/status.handler.ts +216 -0
- package/src/api/handlers/transaction.handler.ts +86 -0
- package/src/api/handlers/types/handler.type.ts +49 -0
- package/src/api/hooks/authorize.hook.test.ts +62 -0
- package/src/api/hooks/authorize.hook.ts +51 -0
- package/src/api/hooks/jwt-auth.hook.test.ts +54 -0
- package/src/api/hooks/jwt-auth.hook.ts +56 -0
- package/src/api/hooks/oauth2-auth.hook.test.ts +173 -0
- package/src/api/hooks/oauth2-auth.hook.ts +46 -0
- package/src/api/hooks/session-header-auth.hook.ts +47 -0
- package/src/api/hooks/session-query-param-auth.hook.ts +46 -0
- package/src/api/hooks/types/hook.type.ts +12 -0
- package/src/api/index.ts +11 -0
- package/src/api/types.ts +3 -0
- package/src/commercetools/api/base-api.ts +28 -0
- package/src/commercetools/api/cart-api.test.ts +95 -0
- package/src/commercetools/api/cart-api.ts +55 -0
- package/src/commercetools/api/custom-type-api.ts +39 -0
- package/src/commercetools/api/order-api.ts +22 -0
- package/src/commercetools/api/payment-api.ts +64 -0
- package/src/commercetools/api/payment-method-api.ts +118 -0
- package/src/commercetools/api/root-api.ts +109 -0
- package/src/commercetools/errors/ct-api.error.ts +27 -0
- package/src/commercetools/helpers/currency.converter.test.ts +121 -0
- package/src/commercetools/helpers/currency.converter.test_data.json +2379 -0
- package/src/commercetools/helpers/currency.converter.ts +102 -0
- package/src/commercetools/helpers/taxrate.converter.test.ts +48 -0
- package/src/commercetools/helpers/taxrate.converter.ts +39 -0
- package/src/commercetools/index.ts +22 -0
- package/src/commercetools/services/ct-cart.service.test.ts +1616 -0
- package/src/commercetools/services/ct-cart.service.ts +316 -0
- package/src/commercetools/services/ct-custom-type.service.ts +62 -0
- package/src/commercetools/services/ct-order.service.test.ts +136 -0
- package/src/commercetools/services/ct-order.service.ts +47 -0
- package/src/commercetools/services/ct-payment-method.service.test.ts +878 -0
- package/src/commercetools/services/ct-payment-method.service.ts +223 -0
- package/src/commercetools/services/ct-payment.service.test.ts +1123 -0
- package/src/commercetools/services/ct-payment.service.ts +574 -0
- package/src/commercetools/types/api.type.ts +162 -0
- package/src/commercetools/types/cart.type.ts +83 -0
- package/src/commercetools/types/custom-type.type.ts +21 -0
- package/src/commercetools/types/order.type.ts +19 -0
- package/src/commercetools/types/payment-method.type.ts +130 -0
- package/src/commercetools/types/payment.type.ts +74 -0
- package/src/commercetools/types/predefined-custom-types.type.ts +240 -0
- package/src/commercetools/types.ts +7 -0
- package/src/errorx/errorx.ts +625 -0
- package/src/errorx/index.ts +1 -0
- package/src/errorx/types.ts +5 -0
- package/src/fetch/decorators/base.decorator.ts +13 -0
- package/src/fetch/decorators/monitoring.decorator.ts +41 -0
- package/src/fetch/fetch.test.ts +57 -0
- package/src/fetch/index.ts +3 -0
- package/src/fetch/types/fetch.type.ts +11 -0
- package/src/fetch/types.ts +1 -0
- package/src/index.ts +15 -0
- package/src/logger/commercetools-logger.ts +69 -0
- package/src/logger/index.ts +1 -0
- package/src/logger/types/logger.type.ts +6 -0
- package/src/logger/types.ts +1 -0
- package/src/mocks/auth.mock.ts +6 -0
- package/src/mocks/cart.mock.ts +239 -0
- package/src/mocks/ct-api-error.mock.ts +23 -0
- package/src/mocks/index.ts +4 -0
- package/src/mocks/payment.mock.ts +35 -0
- package/src/payment-processor.ts +317 -0
- package/src/security/authn/authns.ts +219 -0
- package/src/security/authn/bearer-utils.test.ts +35 -0
- package/src/security/authn/bearer-utils.ts +28 -0
- package/src/security/authn/jwt-authn-manager.ts +69 -0
- package/src/security/authn/oauth2-authn-manager.ts +105 -0
- package/src/security/authn/session-header-authn-manager.ts +58 -0
- package/src/security/authn/session-query-param-authn-manager.ts +53 -0
- package/src/security/authn/types/authn.type.ts +50 -0
- package/src/security/authz/authorization-manager.ts +39 -0
- package/src/security/authz/types/authz.type.ts +13 -0
- package/src/security/index.ts +13 -0
- package/src/security/services/authorization.service.ts +54 -0
- package/src/security/services/jwt.service.test.ts +27 -0
- package/src/security/services/jwt.service.ts +45 -0
- package/src/security/services/oauth2.service.ts +60 -0
- package/src/security/services/session.service.ts +141 -0
- package/src/security/services/types/authorization.type.ts +10 -0
- package/src/security/services/types/jwt.type.ts +3 -0
- package/src/security/services/types/oauth2.type.ts +15 -0
- package/src/security/services/types/session.type.ts +38 -0
- package/src/security/types.ts +6 -0
- package/tsconfig.json +28 -0
- package/tsconfig.prod.json +9 -0
- package/typedoc.json +6 -0
- package/vitest.config.ts +15 -0
- package/wiki/getting-started.md +13 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthType,
|
|
3
|
+
BaseIncomingHttpHeaders,
|
|
4
|
+
BaseOutgoingHttpHeaders,
|
|
5
|
+
HandlerResponse,
|
|
6
|
+
RouteHandler,
|
|
7
|
+
} from '@api-types';
|
|
8
|
+
import type { Logger } from '@logger-types';
|
|
9
|
+
import type { AuthorizationService } from '@security-types';
|
|
10
|
+
|
|
11
|
+
export type HealthCheckResult = {
|
|
12
|
+
name: string;
|
|
13
|
+
status: 'UP' | 'DOWN';
|
|
14
|
+
details?: object;
|
|
15
|
+
message?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type HealthCheck = () => Promise<HealthCheckResult>;
|
|
19
|
+
|
|
20
|
+
export type StatusConfig = {
|
|
21
|
+
timeout: number;
|
|
22
|
+
checks: HealthCheck[];
|
|
23
|
+
metadataFn?: () => Promise<object>;
|
|
24
|
+
log: Logger;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type StatusRequest = undefined;
|
|
28
|
+
|
|
29
|
+
export type StatusResponse = {
|
|
30
|
+
status: 'OK' | 'Partially Available' | 'Unavailable';
|
|
31
|
+
timestamp: string;
|
|
32
|
+
checks: HealthCheckResult[];
|
|
33
|
+
version: string;
|
|
34
|
+
metadata?: object;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type StatusIncomingHttpHeaders = BaseIncomingHttpHeaders & {};
|
|
38
|
+
export type StatusOutgoingHttpHeaders = BaseOutgoingHttpHeaders & {};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if CoCo permissions are available
|
|
42
|
+
* @param opts
|
|
43
|
+
* @returns
|
|
44
|
+
*/
|
|
45
|
+
export const healthCheckCommercetoolsPermissions =
|
|
46
|
+
(opts: {
|
|
47
|
+
ctAuthorizationService: AuthorizationService;
|
|
48
|
+
projectKey: string;
|
|
49
|
+
requiredPermissions: string[];
|
|
50
|
+
}): HealthCheck =>
|
|
51
|
+
async (): Promise<HealthCheckResult> => {
|
|
52
|
+
try {
|
|
53
|
+
const token = await opts.ctAuthorizationService.getAccessToken();
|
|
54
|
+
|
|
55
|
+
const hasManageProjectPermission = token.scope
|
|
56
|
+
.split(' ')
|
|
57
|
+
.includes(`manage_project:${opts.projectKey}`);
|
|
58
|
+
|
|
59
|
+
const foundAll =
|
|
60
|
+
hasManageProjectPermission ||
|
|
61
|
+
opts.requiredPermissions.every((currentScope) =>
|
|
62
|
+
token.scope
|
|
63
|
+
.split(' ')
|
|
64
|
+
.some(
|
|
65
|
+
(scopeInToken: string) =>
|
|
66
|
+
scopeInToken === `${currentScope}:${opts.projectKey}`,
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (foundAll) {
|
|
71
|
+
return {
|
|
72
|
+
name: 'commercetools permissions',
|
|
73
|
+
status: 'UP',
|
|
74
|
+
details: {
|
|
75
|
+
scope: token.scope,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name: 'commercetools permissions',
|
|
82
|
+
status: 'DOWN',
|
|
83
|
+
message: `commercetools permissions are not correct, expected scopes: ${opts.requiredPermissions.join(
|
|
84
|
+
' ',
|
|
85
|
+
)}, actual scopes: ${token.scope}`,
|
|
86
|
+
details: {
|
|
87
|
+
expectedScopes: opts.requiredPermissions,
|
|
88
|
+
actualScopes: token.scope,
|
|
89
|
+
reason: 'scopes not available',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return {
|
|
94
|
+
name: 'commercetools permissions',
|
|
95
|
+
status: 'DOWN',
|
|
96
|
+
message: `Not able to talk with commercetools API`,
|
|
97
|
+
details: {
|
|
98
|
+
error,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export abstract class StatusHandler
|
|
105
|
+
implements
|
|
106
|
+
RouteHandler<
|
|
107
|
+
StatusRequest,
|
|
108
|
+
StatusResponse,
|
|
109
|
+
StatusIncomingHttpHeaders,
|
|
110
|
+
StatusOutgoingHttpHeaders
|
|
111
|
+
>
|
|
112
|
+
{
|
|
113
|
+
public readonly logger: Logger;
|
|
114
|
+
public constructor(opts: { logger: Logger }) {
|
|
115
|
+
this.logger = opts.logger;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public method() {
|
|
119
|
+
return 'GET';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public endpoint() {
|
|
123
|
+
return '/operations/status';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public auth(): AuthType[] {
|
|
127
|
+
return [
|
|
128
|
+
{
|
|
129
|
+
type: 'jwt',
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public async handler(_opts: {
|
|
135
|
+
request: StatusRequest;
|
|
136
|
+
headers: StatusIncomingHttpHeaders;
|
|
137
|
+
}): Promise<HandlerResponse<StatusResponse, StatusOutgoingHttpHeaders>> {
|
|
138
|
+
const statusConfig = this.handleStatus();
|
|
139
|
+
|
|
140
|
+
const status: Partial<StatusResponse> = {
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
version: process.env.npm_package_version ?? '0.0.0',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (statusConfig.metadataFn) {
|
|
146
|
+
status.metadata = await statusConfig.metadataFn();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const timeout = async <T>(ms: number, promise: Promise<T>): Promise<T> => {
|
|
150
|
+
let timeoutId: NodeJS.Timeout;
|
|
151
|
+
|
|
152
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
153
|
+
timeoutId = setTimeout(
|
|
154
|
+
() => reject(new Error(`Timeout after ${ms}ms`)),
|
|
155
|
+
ms,
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
160
|
+
clearTimeout(timeoutId);
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const results = await timeout(
|
|
166
|
+
statusConfig.timeout,
|
|
167
|
+
Promise.all(statusConfig.checks.map((check) => check())),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
let allDown = true;
|
|
171
|
+
let atLeastOneDown = false;
|
|
172
|
+
|
|
173
|
+
results.forEach((result) => {
|
|
174
|
+
if (result.status === 'DOWN') {
|
|
175
|
+
atLeastOneDown = true;
|
|
176
|
+
statusConfig.log.error(
|
|
177
|
+
{ details: result.details },
|
|
178
|
+
result.message ??
|
|
179
|
+
`Health check for ${result.name} failed, check details`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (result.status === 'UP') {
|
|
183
|
+
allDown = false;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (allDown) {
|
|
188
|
+
status.status = 'Unavailable';
|
|
189
|
+
} else if (atLeastOneDown) {
|
|
190
|
+
status.status = 'Partially Available';
|
|
191
|
+
} else {
|
|
192
|
+
status.status = 'OK';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
status.checks = results;
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
status: allDown ? 503 : 200,
|
|
199
|
+
body: status as StatusResponse,
|
|
200
|
+
};
|
|
201
|
+
} catch (e) {
|
|
202
|
+
if (e instanceof Error && e.message === 'Timeout') {
|
|
203
|
+
statusConfig.log.error({ err: e }, 'Health check timed out');
|
|
204
|
+
}
|
|
205
|
+
status.status = 'Unavailable';
|
|
206
|
+
status.checks = [];
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
status: 503,
|
|
210
|
+
body: status as StatusResponse,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public abstract handleStatus(): StatusConfig;
|
|
216
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { UUID } from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
AuthType,
|
|
4
|
+
BaseIncomingHttpHeaders,
|
|
5
|
+
BaseOutgoingHttpHeaders,
|
|
6
|
+
HandlerResponse,
|
|
7
|
+
RouteHandler,
|
|
8
|
+
} from '@api-types';
|
|
9
|
+
import type { Logger } from '@logger-types';
|
|
10
|
+
|
|
11
|
+
export type TransactionRequest = {
|
|
12
|
+
cartId: UUID;
|
|
13
|
+
paymentInterface: UUID;
|
|
14
|
+
checkoutTransactionItemId: UUID;
|
|
15
|
+
amount?: {
|
|
16
|
+
centAmount: number;
|
|
17
|
+
currencyCode: string;
|
|
18
|
+
};
|
|
19
|
+
futureOrderNumber?: string;
|
|
20
|
+
paymentMethod?: {
|
|
21
|
+
id: UUID;
|
|
22
|
+
};
|
|
23
|
+
type?: 'Recurring';
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type TransactionResponse = {
|
|
27
|
+
transactionStatus: {
|
|
28
|
+
state: 'Completed' | 'Failed' | 'Pending';
|
|
29
|
+
errors: {
|
|
30
|
+
code: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}[];
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type TransactionIncomingHttpHeaders = BaseIncomingHttpHeaders & {};
|
|
37
|
+
export type TransactionOutgoingHttpHeaders = BaseOutgoingHttpHeaders & {};
|
|
38
|
+
|
|
39
|
+
export abstract class TransactionHandler
|
|
40
|
+
implements
|
|
41
|
+
RouteHandler<
|
|
42
|
+
TransactionRequest,
|
|
43
|
+
TransactionResponse,
|
|
44
|
+
TransactionIncomingHttpHeaders,
|
|
45
|
+
TransactionOutgoingHttpHeaders
|
|
46
|
+
>
|
|
47
|
+
{
|
|
48
|
+
public readonly logger: Logger;
|
|
49
|
+
public constructor(opts: { logger: Logger }) {
|
|
50
|
+
this.logger = opts.logger;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public method() {
|
|
54
|
+
return 'POST';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public endpoint() {
|
|
58
|
+
return '/operations/transactions';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public auth(): AuthType[] {
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
type: 'oauth2',
|
|
65
|
+
allowedScopes: ['manage_project', 'manage_checkout_transactions'],
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async handler(opts: {
|
|
71
|
+
request: TransactionRequest;
|
|
72
|
+
headers: TransactionIncomingHttpHeaders;
|
|
73
|
+
}): Promise<
|
|
74
|
+
HandlerResponse<TransactionResponse, TransactionOutgoingHttpHeaders>
|
|
75
|
+
> {
|
|
76
|
+
return await this.handleTransaction({
|
|
77
|
+
request: opts.request,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public abstract handleTransaction(opts: {
|
|
82
|
+
request: TransactionRequest;
|
|
83
|
+
}): Promise<
|
|
84
|
+
HandlerResponse<TransactionResponse, TransactionOutgoingHttpHeaders>
|
|
85
|
+
>;
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type HandlerResponse<Response, OutgoingHttpHeaders> = {
|
|
2
|
+
status: number;
|
|
3
|
+
body: Response;
|
|
4
|
+
headers?: OutgoingHttpHeaders;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type AuthType =
|
|
8
|
+
| {
|
|
9
|
+
type: 'jwt';
|
|
10
|
+
}
|
|
11
|
+
| {
|
|
12
|
+
type: 'oauth2';
|
|
13
|
+
allowedScopes: string[];
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
type: 'session';
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface RouteHandler<
|
|
20
|
+
Request,
|
|
21
|
+
Response,
|
|
22
|
+
IncomingHttpHeaders,
|
|
23
|
+
OutgoingHttpHeaders,
|
|
24
|
+
> {
|
|
25
|
+
endpoint: () => string;
|
|
26
|
+
handler: (opts: {
|
|
27
|
+
request: Request;
|
|
28
|
+
headers: IncomingHttpHeaders;
|
|
29
|
+
}) => Promise<HandlerResponse<Response, OutgoingHttpHeaders>>;
|
|
30
|
+
auth: () => AuthType[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type BaseIncomingHttpHeaders = {
|
|
34
|
+
authorization: string;
|
|
35
|
+
'content-type': string;
|
|
36
|
+
accept: string;
|
|
37
|
+
'x-request-id': string;
|
|
38
|
+
'x-correlation-id': string;
|
|
39
|
+
[key: string]: string | string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type BaseOutgoingHttpHeaders = {
|
|
43
|
+
authorization: string;
|
|
44
|
+
'content-type': string;
|
|
45
|
+
accept: string;
|
|
46
|
+
'x-request-id': string;
|
|
47
|
+
'x-correlation-id': string;
|
|
48
|
+
[key: string]: string | string[];
|
|
49
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ContextProvider, RequestContextData } from '@api-types';
|
|
2
|
+
import { ErrorAuthErrorResponse } from '@errorx';
|
|
3
|
+
import type { Logger } from '@logger-types';
|
|
4
|
+
import type { AuthorityAuthorizationManager } from '@security';
|
|
5
|
+
import { AuthorityAuthorizationHook } from '@/api/hooks/authorize.hook.js';
|
|
6
|
+
|
|
7
|
+
describe('AuthorityAuthorizationHook', () => {
|
|
8
|
+
const loggerMock = vi.fn() as unknown as Logger;
|
|
9
|
+
const authorizationManagerMock = {
|
|
10
|
+
verify: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
const contextProviderMock = {
|
|
13
|
+
getContextData: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const hook = new AuthorityAuthorizationHook({
|
|
17
|
+
authorizationManager:
|
|
18
|
+
authorizationManagerMock as unknown as AuthorityAuthorizationManager,
|
|
19
|
+
contextProvider:
|
|
20
|
+
contextProviderMock as unknown as ContextProvider<RequestContextData>,
|
|
21
|
+
logger: loggerMock,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('authorize should throw an error if authentication is missing', async () => {
|
|
29
|
+
// Arrange
|
|
30
|
+
const authorities = ['admin'];
|
|
31
|
+
//authorizationManagerMock.verify.mockRejectedValue(new ErrorAuthErrorResponse('Unauthorized'));
|
|
32
|
+
contextProviderMock.getContextData.mockReturnValue({});
|
|
33
|
+
|
|
34
|
+
// Act
|
|
35
|
+
const authorizeFn = hook.authorize(...authorities);
|
|
36
|
+
|
|
37
|
+
// Assert
|
|
38
|
+
await expect(authorizeFn()).rejects.toThrow(ErrorAuthErrorResponse);
|
|
39
|
+
expect(authorizationManagerMock.verify).not.toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('authorize should call authorizationManager.verify with the correct arguments', async () => {
|
|
43
|
+
// Arrange
|
|
44
|
+
const authorities = ['admin'];
|
|
45
|
+
const authn = {
|
|
46
|
+
/* mock authentication object */
|
|
47
|
+
};
|
|
48
|
+
contextProviderMock.getContextData = vi
|
|
49
|
+
.fn()
|
|
50
|
+
.mockReturnValue({ authentication: authn });
|
|
51
|
+
|
|
52
|
+
// Act
|
|
53
|
+
const authorizeFn = hook.authorize(...authorities);
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
await authorizeFn();
|
|
57
|
+
expect(authorizationManagerMock.verify).toHaveBeenCalledWith(
|
|
58
|
+
authn,
|
|
59
|
+
authorities,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthorizationHook,
|
|
3
|
+
ContextProvider,
|
|
4
|
+
RequestContextData,
|
|
5
|
+
} from '@api-types';
|
|
6
|
+
import { ErrorAuthErrorResponse } from '@errorx';
|
|
7
|
+
import type { Logger } from '@logger-types';
|
|
8
|
+
import type { AuthorityAuthorizationManager } from '@security';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents an authorization hook that verifies the authority of a request.
|
|
12
|
+
*/
|
|
13
|
+
export class AuthorityAuthorizationHook implements AuthorizationHook {
|
|
14
|
+
private authorizationManager: AuthorityAuthorizationManager;
|
|
15
|
+
private contextProvider: ContextProvider<RequestContextData>;
|
|
16
|
+
private logger: Logger;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Constructs a new instance of the AuthorityAuthorizationHook class.
|
|
20
|
+
* @param opts - The options for configuring the hook.
|
|
21
|
+
*/
|
|
22
|
+
constructor(opts: {
|
|
23
|
+
authorizationManager: AuthorityAuthorizationManager;
|
|
24
|
+
contextProvider: ContextProvider<RequestContextData>;
|
|
25
|
+
logger: Logger;
|
|
26
|
+
}) {
|
|
27
|
+
this.authorizationManager = opts.authorizationManager;
|
|
28
|
+
this.contextProvider = opts.contextProvider;
|
|
29
|
+
this.logger = opts.logger;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Authorizes the request based on the provided authorities.
|
|
34
|
+
* @param authorities - The authorities to verify.
|
|
35
|
+
* @returns A function that performs the authorization check.
|
|
36
|
+
*/
|
|
37
|
+
authorize(...authorities: string[]) {
|
|
38
|
+
return async () => {
|
|
39
|
+
const authn = this.contextProvider.getContextData().authentication;
|
|
40
|
+
|
|
41
|
+
if (!authn) {
|
|
42
|
+
throw new ErrorAuthErrorResponse('Authentication is required.', {
|
|
43
|
+
privateMessage:
|
|
44
|
+
'Not able to authenticate the request. Missing authentication information in context.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.authorizationManager.verify(authn, authorities);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Logger } from '@logger-types';
|
|
2
|
+
import { JWTAuthentication, type JWTAuthenticationManager } from '@security';
|
|
3
|
+
import type {
|
|
4
|
+
ContextProvider,
|
|
5
|
+
RequestContextData,
|
|
6
|
+
} from '@/api/context/types/request-context.type.js';
|
|
7
|
+
import { JWTAuthenticationHook } from '@/api/hooks/jwt-auth.hook.js';
|
|
8
|
+
|
|
9
|
+
describe('JWTAuthenticationHook', () => {
|
|
10
|
+
const authenticationManager = {
|
|
11
|
+
authenticate: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
const contextProvider = {
|
|
14
|
+
updateContextData: vi.fn(),
|
|
15
|
+
};
|
|
16
|
+
const logger = vi.fn() as unknown as Logger;
|
|
17
|
+
const authenticationHook = new JWTAuthenticationHook({
|
|
18
|
+
authenticationManager:
|
|
19
|
+
authenticationManager as unknown as JWTAuthenticationManager,
|
|
20
|
+
contextProvider:
|
|
21
|
+
contextProvider as unknown as ContextProvider<RequestContextData>,
|
|
22
|
+
logger,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('authenticate should update the context data with authentication', async () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const header = 'Bearer <jwt-token>';
|
|
32
|
+
const request = {
|
|
33
|
+
headers: {
|
|
34
|
+
authorization: 'Bearer <jwt-token>',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const jwtAuthentication = new JWTAuthentication('<jwt-token>', {
|
|
38
|
+
mcCustomerId: '123',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
authenticationManager.authenticate.mockResolvedValue(jwtAuthentication);
|
|
43
|
+
const authenticateFn = authenticationHook.authenticate();
|
|
44
|
+
await authenticateFn(request);
|
|
45
|
+
|
|
46
|
+
// Assert
|
|
47
|
+
expect(contextProvider.updateContextData).toHaveBeenCalledWith({
|
|
48
|
+
authentication: jwtAuthentication,
|
|
49
|
+
});
|
|
50
|
+
expect(authenticationManager.authenticate).toHaveBeenCalledWith({
|
|
51
|
+
authHeader: header,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from 'node:http';
|
|
2
|
+
import type { Logger } from '@logger-types';
|
|
3
|
+
import {
|
|
4
|
+
HeaderBasedAuthentication,
|
|
5
|
+
type JWTAuthenticationManager,
|
|
6
|
+
} from '@security';
|
|
7
|
+
import type {
|
|
8
|
+
ContextProvider,
|
|
9
|
+
RequestContextData,
|
|
10
|
+
} from '@/api/context/types/request-context.type.js';
|
|
11
|
+
import type { AuthenticationHook } from '@/api/hooks/types/hook.type.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents a JWT Authentication Hook.
|
|
15
|
+
*/
|
|
16
|
+
export class JWTAuthenticationHook implements AuthenticationHook {
|
|
17
|
+
private authenticationManager: JWTAuthenticationManager;
|
|
18
|
+
private contextProvider: ContextProvider<RequestContextData>;
|
|
19
|
+
private logger: Logger;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Constructs a new instance of the JWTAuthenticationHook class.
|
|
23
|
+
* @param opts - The options for the JWTAuthenticationHook.
|
|
24
|
+
*/
|
|
25
|
+
constructor(opts: {
|
|
26
|
+
authenticationManager: JWTAuthenticationManager;
|
|
27
|
+
contextProvider: ContextProvider<RequestContextData>;
|
|
28
|
+
logger: Logger;
|
|
29
|
+
}) {
|
|
30
|
+
this.authenticationManager = opts.authenticationManager;
|
|
31
|
+
this.contextProvider = opts.contextProvider;
|
|
32
|
+
this.logger = opts.logger;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Authenticates the request.
|
|
37
|
+
* @returns A function that performs the authentication.
|
|
38
|
+
*/
|
|
39
|
+
authenticate() {
|
|
40
|
+
return async (request: {
|
|
41
|
+
headers: IncomingHttpHeaders;
|
|
42
|
+
query?: unknown;
|
|
43
|
+
}) => {
|
|
44
|
+
const authorizationHeader = new HeaderBasedAuthentication(
|
|
45
|
+
request.headers.authorization as string,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const authn =
|
|
49
|
+
await this.authenticationManager.authenticate(authorizationHeader);
|
|
50
|
+
|
|
51
|
+
this.contextProvider.updateContextData({
|
|
52
|
+
authentication: authn,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|