@unchainedshop/api 4.6.2 → 4.7.2
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/lib/acl.js +29 -1
- package/lib/api-index.d.ts +1 -0
- package/lib/auth.js +1 -0
- package/lib/context.d.ts +1 -0
- package/lib/context.js +8 -3
- package/lib/events.d.ts +2 -0
- package/lib/events.js +2 -0
- package/lib/express/index.d.ts +6 -9
- package/lib/express/index.js +52 -125
- package/lib/fastify/index.d.ts +6 -9
- package/lib/fastify/index.js +51 -113
- package/lib/mcp/tools/localization/getLocalizationsConfig.d.ts +1 -1
- package/lib/mcp/tools/localization/schemas.d.ts +34 -0
- package/lib/mcp/tools/order/handlers/getTopCustomers.d.ts +2 -0
- package/lib/mcp/tools/quotation/handlers/getQuotation.d.ts +2 -0
- package/lib/mcp/tools/quotation/handlers/listQuotations.d.ts +2 -0
- package/lib/mcp/tools/quotation/handlers/makeQuotationProposal.d.ts +2 -0
- package/lib/mcp/tools/quotation/handlers/rejectQuotation.d.ts +2 -0
- package/lib/mcp/tools/quotation/handlers/verifyQuotation.d.ts +2 -0
- package/lib/mcp/tools/system/handlers/countEvents.d.ts +8 -1
- package/lib/mcp/tools/system/handlers/countWork.d.ts +10 -1
- package/lib/mcp/tools/system/handlers/countWork.js +1 -0
- package/lib/mcp/tools/system/handlers/index.d.ts +4 -4
- package/lib/mcp/tools/system/handlers/listEvents.d.ts +12 -1
- package/lib/mcp/tools/system/handlers/listWork.d.ts +14 -1
- package/lib/mcp/tools/system/handlers/listWork.js +1 -0
- package/lib/mcp/tools/users/handlers/addUserEmail.d.ts +2 -0
- package/lib/mcp/tools/users/handlers/createUser.d.ts +2 -0
- package/lib/mcp/tools/users/handlers/enrollUser.d.ts +2 -0
- package/lib/mcp/tools/users/handlers/getCurrentUser.d.ts +2 -0
- package/lib/mcp/tools/users/handlers/getUser.d.ts +2 -0
- package/lib/mcp/tools/users/handlers/listUsers.d.ts +2 -0
- package/lib/mcp/tools/users/handlers/setUserTags.d.ts +2 -0
- package/lib/mcp/tools/users/handlers/setUserUsername.d.ts +2 -0
- package/lib/mcp/tools/users/handlers/updateUser.d.ts +2 -0
- package/lib/mcp/utils/getNormalizedQuotationDetails.d.ts +2 -0
- package/lib/mcp/utils/getNormalizedUserDetails.d.ts +2 -0
- package/lib/mcp/utils/normalizeMediaUrl.js +1 -1
- package/lib/mcp/utils/sanitizeLocalizationEntityData.d.ts +1 -1
- package/lib/mcp/utils/validateIsoCode.d.ts +1 -1
- package/lib/resolvers/mutations/index.d.ts +1 -6
- package/lib/resolvers/mutations/index.js +2 -12
- package/lib/resolvers/mutations/orders/updateCart.js +7 -1
- package/lib/resolvers/queries/orders/orders.js +2 -2
- package/lib/resolvers/type/index.d.ts +0 -1
- package/lib/resolvers/type/order/order-delivery-pickup-types.d.ts +0 -1
- package/lib/resolvers/type/order/order-delivery-pickup-types.js +0 -7
- package/lib/roles/index.js +1 -0
- package/lib/roles/loggedIn.js +1 -0
- package/lib/schema/mutation.js +6 -44
- package/lib/schema/types/index.js +2 -2
- package/lib/schema/types/order/delivery.js +0 -2
- package/package.json +8 -17
package/lib/acl.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { Roles } from '@unchainedshop/roles';
|
|
2
|
+
import { emit } from '@unchainedshop/events';
|
|
2
3
|
import { NoPermissionError, PermissionSystemError } from "./errors.js";
|
|
4
|
+
import { API_EVENTS } from "./events.js";
|
|
5
|
+
const SENSITIVE_ACTION_PREFIXES = [
|
|
6
|
+
'manageUser',
|
|
7
|
+
'login',
|
|
8
|
+
'logout',
|
|
9
|
+
'reset',
|
|
10
|
+
'forgot',
|
|
11
|
+
'impersonate',
|
|
12
|
+
'updateUser',
|
|
13
|
+
'createUser',
|
|
14
|
+
];
|
|
3
15
|
const defaultOptions = {
|
|
4
16
|
showKey: true,
|
|
5
17
|
mapArgs: (...args) => args,
|
|
@@ -19,11 +31,27 @@ export const ensureIsFunction = (fn, action, options, key) => {
|
|
|
19
31
|
});
|
|
20
32
|
}
|
|
21
33
|
};
|
|
34
|
+
const isSensitiveAction = (action) => {
|
|
35
|
+
return SENSITIVE_ACTION_PREFIXES.some((prefix) => action.startsWith(prefix));
|
|
36
|
+
};
|
|
22
37
|
const checkAction = async (context, action, args = emptyArray, options = emptyObject) => {
|
|
23
38
|
const { key } = options || emptyObject;
|
|
24
39
|
const hasPermission = await Roles.userHasPermission(context, action, args);
|
|
25
|
-
if (hasPermission)
|
|
40
|
+
if (hasPermission) {
|
|
41
|
+
if (isSensitiveAction(action)) {
|
|
42
|
+
await emit(API_EVENTS.ACL_GRANTED_SENSITIVE, {
|
|
43
|
+
userId: context.userId,
|
|
44
|
+
action,
|
|
45
|
+
key,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
26
48
|
return;
|
|
49
|
+
}
|
|
50
|
+
await emit(API_EVENTS.ACL_DENIED, {
|
|
51
|
+
userId: context.userId,
|
|
52
|
+
action,
|
|
53
|
+
key,
|
|
54
|
+
});
|
|
27
55
|
const keyText = key && key !== '' ? ` in "${key}"` : '';
|
|
28
56
|
throw new NoPermissionError({
|
|
29
57
|
userId: context.userId,
|
package/lib/api-index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './loaders/index.ts';
|
|
|
8
8
|
export * from './errors.ts';
|
|
9
9
|
export * as acl from './acl.ts';
|
|
10
10
|
export * as roles from './roles/index.ts';
|
|
11
|
+
export type { OIDCProviderConfig, AuthConfig } from './auth.ts';
|
|
11
12
|
export { createContextResolver, getCurrentContextResolver, setCurrentContextResolver };
|
|
12
13
|
export type UnchainedServerOptions = {
|
|
13
14
|
roles?: any;
|
package/lib/auth.js
CHANGED
|
@@ -73,6 +73,7 @@ export async function verifyLocalToken(token) {
|
|
|
73
73
|
logger.debug('Token expired');
|
|
74
74
|
}
|
|
75
75
|
else if (error instanceof jose.errors.JWTInvalid ||
|
|
76
|
+
error instanceof jose.errors.JWSInvalid ||
|
|
76
77
|
error instanceof jose.errors.JWSSignatureVerificationFailed ||
|
|
77
78
|
error instanceof jose.errors.JWTClaimValidationFailed) {
|
|
78
79
|
logger.debug('Invalid token signature or claims');
|
package/lib/context.d.ts
CHANGED
package/lib/context.js
CHANGED
|
@@ -6,7 +6,7 @@ export const getCurrentContextResolver = () => context;
|
|
|
6
6
|
export const setCurrentContextResolver = (newContext) => {
|
|
7
7
|
context = newContext;
|
|
8
8
|
};
|
|
9
|
-
export const createContextResolver = (unchainedAPI, unchainedConfig) => async ({ getHeader, setHeader, remoteAddress, remotePort, userId, impersonatorId, accessToken, login, logout, }) => {
|
|
9
|
+
export const createContextResolver = (unchainedAPI, unchainedConfig) => async ({ getHeader, setHeader, remoteAddress, remotePort, userId, impersonatorId, accessToken, tokenVersion, login, logout, }) => {
|
|
10
10
|
const abstractHttpServerContext = { remoteAddress, remotePort, getHeader, setHeader };
|
|
11
11
|
const loaders = instantiateLoaders(unchainedAPI);
|
|
12
12
|
const localeContext = await getLocaleContext(abstractHttpServerContext, unchainedAPI);
|
|
@@ -21,8 +21,13 @@ export const createContextResolver = (unchainedAPI, unchainedConfig) => async ({
|
|
|
21
21
|
if (userId && !userContext.userId) {
|
|
22
22
|
const user = await unchainedAPI.modules.users.findUserById(userId);
|
|
23
23
|
if (user) {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const userTokenVersion = user.tokenVersion ?? 1;
|
|
25
|
+
if (tokenVersion !== undefined && tokenVersion !== userTokenVersion) {
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
userContext.user = user;
|
|
29
|
+
userContext.userId = user._id;
|
|
30
|
+
}
|
|
26
31
|
}
|
|
27
32
|
}
|
|
28
33
|
return {
|
package/lib/events.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const API_EVENTS: {
|
|
2
2
|
readonly API_LOGIN_TOKEN_CREATED: "API_LOGIN_TOKEN_CREATED";
|
|
3
3
|
readonly API_LOGOUT: "API_LOGOUT";
|
|
4
|
+
readonly ACL_DENIED: "ACL_DENIED";
|
|
5
|
+
readonly ACL_GRANTED_SENSITIVE: "ACL_GRANTED_SENSITIVE";
|
|
4
6
|
};
|
|
5
7
|
export type API_EVENTS = (typeof API_EVENTS)[keyof typeof API_EVENTS];
|
package/lib/events.js
CHANGED
package/lib/express/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import e from 'express';
|
|
2
2
|
import type { YogaServerInstance } from 'graphql-yoga';
|
|
3
|
-
import { mongodb } from '@unchainedshop/mongodb';
|
|
4
3
|
import type { UnchainedCore } from '@unchainedshop/core';
|
|
4
|
+
import type { AuthConfig } from '../auth.ts';
|
|
5
5
|
import type { ChatConfiguration } from '../chat/utils.ts';
|
|
6
6
|
import { connectChat } from './chatHandler.ts';
|
|
7
7
|
export interface AdminUIRouterOptions {
|
|
@@ -9,17 +9,14 @@ export interface AdminUIRouterOptions {
|
|
|
9
9
|
enabled?: boolean;
|
|
10
10
|
}
|
|
11
11
|
export declare const adminUIRouter: (enabled?: boolean) => import("express-serve-static-core").Router;
|
|
12
|
-
export declare const connect: (expressApp: e.Express, { graphqlHandler,
|
|
12
|
+
export declare const connect: (expressApp: e.Express, { graphqlHandler, unchainedAPI, }: {
|
|
13
13
|
graphqlHandler: YogaServerInstance<any, any>;
|
|
14
|
-
db: mongodb.Db;
|
|
15
14
|
unchainedAPI: UnchainedCore;
|
|
16
|
-
}, { allowRemoteToLocalhostSecureCookies, adminUI, chat,
|
|
15
|
+
}, { allowRemoteToLocalhostSecureCookies, adminUI, chat, authConfig, trustProxy, }?: {
|
|
17
16
|
allowRemoteToLocalhostSecureCookies?: boolean;
|
|
18
17
|
adminUI?: boolean | Omit<AdminUIRouterOptions, "enabled">;
|
|
19
18
|
chat?: ChatConfiguration;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}) => void;
|
|
24
|
-
export declare const expressRouter: (enabled?: boolean) => import("express-serve-static-core").Router;
|
|
19
|
+
authConfig?: AuthConfig;
|
|
20
|
+
trustProxy?: boolean;
|
|
21
|
+
}) => Promise<void>;
|
|
25
22
|
export { connectChat };
|
package/lib/express/index.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import e from 'express';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import MongoStore from "../mongo-store.js";
|
|
5
|
-
import { Passport } from 'passport';
|
|
6
|
-
import { mongodb } from '@unchainedshop/mongodb';
|
|
7
|
-
import { emit } from '@unchainedshop/events';
|
|
2
|
+
import cookieParser from 'cookie-parser';
|
|
3
|
+
import { pluginRegistry } from '@unchainedshop/core';
|
|
8
4
|
import { getCurrentContextResolver } from "../context.js";
|
|
9
|
-
import
|
|
10
|
-
import createERCMetadataMiddleware from "./createERCMetadataMiddleware.js";
|
|
11
|
-
import createTempUploadMiddleware from "./createTempUploadMiddleware.js";
|
|
5
|
+
import { createAuthContext } from "../middleware/createAuthMiddleware.js";
|
|
12
6
|
import createMCPMiddleware from "./createMCPMiddleware.js";
|
|
13
|
-
import { API_EVENTS } from "../events.js";
|
|
14
7
|
import { connectChat } from "./chatHandler.js";
|
|
8
|
+
import { mountRoutes } from "./mountRoutes.js";
|
|
9
|
+
import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
|
|
15
10
|
export const adminUIRouter = (enabled = true) => {
|
|
16
11
|
const router = e.Router();
|
|
17
12
|
const staticURL = import.meta.resolve('@unchainedshop/admin-ui');
|
|
@@ -24,87 +19,53 @@ export const adminUIRouter = (enabled = true) => {
|
|
|
24
19
|
}
|
|
25
20
|
return router;
|
|
26
21
|
};
|
|
27
|
-
const resolveUserRemoteAddress = (req) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
req.
|
|
22
|
+
const resolveUserRemoteAddress = (req, trustProxy = false) => {
|
|
23
|
+
let remoteAddress;
|
|
24
|
+
if (trustProxy) {
|
|
25
|
+
const forwardedFor = req.headers['x-forwarded-for'];
|
|
26
|
+
const forwardedIps = forwardedFor?.split(',').map((ip) => ip.trim());
|
|
27
|
+
remoteAddress =
|
|
28
|
+
req.headers['x-real-ip'] ||
|
|
29
|
+
forwardedIps?.[forwardedIps.length - 1] ||
|
|
30
|
+
req.socket?.remoteAddress;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
remoteAddress = req.socket?.remoteAddress;
|
|
34
|
+
}
|
|
31
35
|
const remotePort = req.socket?.remotePort;
|
|
32
36
|
return { remoteAddress, remotePort };
|
|
33
37
|
};
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const { BULK_IMPORT_API_PATH = '/bulk-import', ERC_METADATA_API_PATH = '/erc-metadata', TEMP_UPLOAD_API_PATH = '/temp-upload', MCP_API_PATH = '/mcp', GRAPHQL_API_PATH = '/graphql', UNCHAINED_COOKIE_NAME = 'unchained_token', UNCHAINED_COOKIE_PATH = '/', UNCHAINED_COOKIE_DOMAIN, UNCHAINED_COOKIE_SAMESITE = 'none', UNCHAINED_COOKIE_INSECURE, } = process.env;
|
|
37
|
-
const addContext = async function middlewareWithContext(req, res, next) {
|
|
38
|
+
const { MCP_API_PATH = '/mcp' } = process.env;
|
|
39
|
+
const createAddContextMiddleware = (authConfig, trustProxy = false) => async function middlewareWithContext(req, res, next) {
|
|
38
40
|
try {
|
|
39
41
|
const setHeader = (key, value) => res.setHeader(key, value);
|
|
40
42
|
const getHeader = (key) => req.headers[key];
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
const tokenObject = {
|
|
55
|
-
_id: req.sessionID,
|
|
56
|
-
userId: user._id,
|
|
57
|
-
tokenExpires: new Date(req.session?.cookie._expires),
|
|
58
|
-
};
|
|
59
|
-
await emit(API_EVENTS.API_LOGIN_TOKEN_CREATED, tokenObject);
|
|
60
|
-
user._inLoginMethodResponse = true;
|
|
61
|
-
return { user, ...tokenObject };
|
|
62
|
-
};
|
|
63
|
-
const logout = async (sessionId) => {
|
|
64
|
-
const { user } = req;
|
|
65
|
-
if (!user)
|
|
66
|
-
return false;
|
|
67
|
-
const currentSessionId = req.sessionID;
|
|
68
|
-
const targetSessionId = sessionId || currentSessionId;
|
|
69
|
-
const tokenObject = {
|
|
70
|
-
_id: targetSessionId,
|
|
71
|
-
userId: user._id,
|
|
72
|
-
};
|
|
73
|
-
if (sessionId && sessionId !== currentSessionId) {
|
|
74
|
-
await new Promise((resolve, reject) => {
|
|
75
|
-
req.sessionStore.destroy(sessionId, (error) => {
|
|
76
|
-
if (error) {
|
|
77
|
-
return reject(error);
|
|
78
|
-
}
|
|
79
|
-
return resolve();
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
await new Promise((resolve, reject) => {
|
|
85
|
-
req.logout((error, result) => {
|
|
86
|
-
if (error) {
|
|
87
|
-
return reject(error);
|
|
88
|
-
}
|
|
89
|
-
req.session.impersonatorId = null;
|
|
90
|
-
return resolve(result);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
await emit(API_EVENTS.API_LOGOUT, tokenObject);
|
|
95
|
-
return true;
|
|
43
|
+
const getCookie = (name) => req.cookies?.[name];
|
|
44
|
+
const setCookie = (name, value, options) => res.cookie(name, value, options);
|
|
45
|
+
const clearCookie = (name, options) => res.clearCookie(name, { ...options, maxAge: 0 });
|
|
46
|
+
const { remoteAddress, remotePort } = resolveUserRemoteAddress(req, trustProxy);
|
|
47
|
+
const authContextParams = {
|
|
48
|
+
setHeader,
|
|
49
|
+
getHeader,
|
|
50
|
+
getCookie,
|
|
51
|
+
setCookie,
|
|
52
|
+
clearCookie,
|
|
53
|
+
remoteAddress,
|
|
54
|
+
remotePort,
|
|
96
55
|
};
|
|
97
|
-
const
|
|
56
|
+
const authContext = await createAuthContext(authContextParams, authConfig);
|
|
57
|
+
const context = getCurrentContextResolver();
|
|
98
58
|
req.unchainedContext = await context({
|
|
99
59
|
setHeader,
|
|
100
60
|
getHeader,
|
|
101
61
|
remoteAddress,
|
|
102
62
|
remotePort,
|
|
103
|
-
login,
|
|
104
|
-
logout,
|
|
105
|
-
accessToken,
|
|
106
|
-
userId:
|
|
107
|
-
impersonatorId:
|
|
63
|
+
login: authContext.login,
|
|
64
|
+
logout: authContext.logout,
|
|
65
|
+
accessToken: authContext.accessToken,
|
|
66
|
+
userId: authContext.userId,
|
|
67
|
+
impersonatorId: authContext.impersonatorId,
|
|
68
|
+
tokenVersion: authContext.tokenVersion,
|
|
108
69
|
}, req, res);
|
|
109
70
|
next();
|
|
110
71
|
}
|
|
@@ -112,8 +73,12 @@ const addContext = async function middlewareWithContext(req, res, next) {
|
|
|
112
73
|
next(error);
|
|
113
74
|
}
|
|
114
75
|
};
|
|
115
|
-
export const connect = (expressApp, { graphqlHandler,
|
|
76
|
+
export const connect = async (expressApp, { graphqlHandler, unchainedAPI, }, { allowRemoteToLocalhostSecureCookies = false, adminUI = false, chat, authConfig, trustProxy = false, } = {}) => {
|
|
116
77
|
if (allowRemoteToLocalhostSecureCookies) {
|
|
78
|
+
if (process.env.NODE_ENV === 'production') {
|
|
79
|
+
throw new Error('allowRemoteToLocalhostSecureCookies is not allowed in production. ' +
|
|
80
|
+
'Configure a proper CORS policy with specific allowed origins instead.');
|
|
81
|
+
}
|
|
117
82
|
expressApp.set('trust proxy', 1);
|
|
118
83
|
expressApp.use((req, res, next) => {
|
|
119
84
|
req.headers['x-forwarded-proto'] = 'https';
|
|
@@ -125,59 +90,21 @@ export const connect = (expressApp, { graphqlHandler, db, unchainedAPI, }, { all
|
|
|
125
90
|
next();
|
|
126
91
|
});
|
|
127
92
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
passport.deserializeUser(function deserialize(_id, done) {
|
|
133
|
-
done(null, { _id });
|
|
134
|
-
});
|
|
135
|
-
const name = UNCHAINED_COOKIE_NAME;
|
|
136
|
-
const domain = UNCHAINED_COOKIE_DOMAIN;
|
|
137
|
-
const path = UNCHAINED_COOKIE_PATH;
|
|
138
|
-
const secure = UNCHAINED_COOKIE_INSECURE ? false : true;
|
|
139
|
-
const sameSite = ({
|
|
140
|
-
none: 'none',
|
|
141
|
-
lax: 'lax',
|
|
142
|
-
strict: 'strict',
|
|
143
|
-
'1': true,
|
|
144
|
-
'0': false,
|
|
145
|
-
}[UNCHAINED_COOKIE_SAMESITE?.trim()?.toLowerCase()] || false);
|
|
146
|
-
expressApp.use(session({
|
|
147
|
-
secret: process.env.UNCHAINED_TOKEN_SECRET,
|
|
148
|
-
store: MongoStore.create({
|
|
149
|
-
client: db.client,
|
|
150
|
-
dbName: db.databaseName,
|
|
151
|
-
collectionName: 'sessions',
|
|
152
|
-
touchAfter: 24 * 3600,
|
|
153
|
-
}),
|
|
154
|
-
name,
|
|
155
|
-
saveUninitialized: false,
|
|
156
|
-
resave: false,
|
|
157
|
-
cookie: {
|
|
158
|
-
domain,
|
|
159
|
-
path,
|
|
160
|
-
sameSite,
|
|
161
|
-
secure,
|
|
162
|
-
httpOnly: true,
|
|
163
|
-
maxAge: 1000 * 60 * 60 * 24 * 7,
|
|
164
|
-
},
|
|
165
|
-
}), passport.initialize(), passport.session(), addContext);
|
|
166
|
-
expressApp.use(GRAPHQL_API_PATH, graphqlHandler.handle);
|
|
167
|
-
expressApp.use(ERC_METADATA_API_PATH, createERCMetadataMiddleware);
|
|
168
|
-
expressApp.use(BULK_IMPORT_API_PATH, createBulkImportMiddleware);
|
|
169
|
-
expressApp.use(TEMP_UPLOAD_API_PATH, upload.any(), createTempUploadMiddleware);
|
|
93
|
+
expressApp.use(cookieParser());
|
|
94
|
+
expressApp.use(createAddContextMiddleware(authConfig, trustProxy || allowRemoteToLocalhostSecureCookies));
|
|
95
|
+
expressApp.use(graphqlHandler.graphqlEndpoint, graphqlHandler.handle);
|
|
170
96
|
expressApp.use(MCP_API_PATH, e.json({ limit: '10mb' }));
|
|
171
97
|
expressApp.use(MCP_API_PATH, createMCPMiddleware);
|
|
172
98
|
if (chat) {
|
|
173
99
|
connectChat(expressApp, chat);
|
|
174
100
|
}
|
|
175
|
-
|
|
176
|
-
|
|
101
|
+
const routes = pluginRegistry.getRoutes();
|
|
102
|
+
if (authConfig?.oidcProviders?.length) {
|
|
103
|
+
routes.push(createBackchannelLogoutRoute(authConfig.oidcProviders));
|
|
177
104
|
}
|
|
105
|
+
mountRoutes(expressApp, unchainedAPI, routes);
|
|
178
106
|
if (adminUI) {
|
|
179
107
|
expressApp.use(typeof adminUI === 'object' ? adminUI.prefix : '/', adminUIRouter(true));
|
|
180
108
|
}
|
|
181
109
|
};
|
|
182
|
-
export const expressRouter = adminUIRouter;
|
|
183
110
|
export { connectChat };
|
package/lib/fastify/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { AuthConfig } from '../auth.ts';
|
|
1
2
|
import type { YogaServerInstance } from 'graphql-yoga';
|
|
2
|
-
import type { mongodb } from '@unchainedshop/mongodb';
|
|
3
3
|
import type { UnchainedCore } from '@unchainedshop/core';
|
|
4
4
|
import type { FastifyBaseLogger, FastifyInstance, FastifyPluginAsync } from 'fastify';
|
|
5
5
|
import { connectChat } from './chatHandler.ts';
|
|
@@ -9,18 +9,15 @@ export interface AdminUIRouterOptions {
|
|
|
9
9
|
enabled?: boolean;
|
|
10
10
|
}
|
|
11
11
|
export declare const unchainedLogger: (prefix: string) => FastifyBaseLogger;
|
|
12
|
-
export declare const connect: (fastify: FastifyInstance, { graphqlHandler,
|
|
12
|
+
export declare const connect: (fastify: FastifyInstance, { graphqlHandler, unchainedAPI, }: {
|
|
13
13
|
graphqlHandler: YogaServerInstance<any, any>;
|
|
14
|
-
db: mongodb.Db;
|
|
15
14
|
unchainedAPI: UnchainedCore;
|
|
16
|
-
}, { allowRemoteToLocalhostSecureCookies, adminUI, chat,
|
|
15
|
+
}, { allowRemoteToLocalhostSecureCookies, adminUI, chat, authConfig, trustProxy, }?: {
|
|
17
16
|
allowRemoteToLocalhostSecureCookies?: boolean;
|
|
18
17
|
adminUI?: boolean | Omit<AdminUIRouterOptions, "enabled">;
|
|
19
18
|
chat?: ChatConfiguration;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}) => void;
|
|
19
|
+
authConfig?: AuthConfig;
|
|
20
|
+
trustProxy?: boolean;
|
|
21
|
+
}) => Promise<void>;
|
|
24
22
|
export declare const adminUIRouter: FastifyPluginAsync<AdminUIRouterOptions>;
|
|
25
|
-
export declare const fastifyRouter: FastifyPluginAsync<AdminUIRouterOptions>;
|
|
26
23
|
export { connectChat };
|
package/lib/fastify/index.js
CHANGED
|
@@ -1,64 +1,59 @@
|
|
|
1
1
|
import { getCurrentContextResolver } from "../context.js";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import MongoStore from "../mongo-store.js";
|
|
5
|
-
import { emit } from '@unchainedshop/events';
|
|
6
|
-
import { API_EVENTS } from "../events.js";
|
|
7
|
-
import fastifySession from '@fastify/session';
|
|
2
|
+
import { createAuthContext } from "../middleware/createAuthMiddleware.js";
|
|
3
|
+
import { pluginRegistry } from '@unchainedshop/core';
|
|
8
4
|
import fastifyCookie from '@fastify/cookie';
|
|
9
|
-
import fastifyMultipart from '@fastify/multipart';
|
|
10
5
|
import { createLogger } from '@unchainedshop/logger';
|
|
11
6
|
import mcpHandler from "./mcpHandler.js";
|
|
12
|
-
import tempUploadHandler from "./tempUploadHandler.js";
|
|
13
7
|
import { connectChat } from "./chatHandler.js";
|
|
8
|
+
import { mountRoutes } from "./mountRoutes.js";
|
|
14
9
|
import { readFileSync } from 'node:fs';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
import { createBackchannelLogoutRoute } from "../handlers/createBackchannelLogoutHandler.js";
|
|
11
|
+
const resolveUserRemoteAddress = (req, trustProxy = false) => {
|
|
12
|
+
let remoteAddress;
|
|
13
|
+
if (trustProxy) {
|
|
14
|
+
const forwardedFor = req.headers['x-forwarded-for'];
|
|
15
|
+
const forwardedIps = forwardedFor?.split(',').map((ip) => ip.trim());
|
|
16
|
+
remoteAddress =
|
|
17
|
+
req.headers['x-real-ip'] ||
|
|
18
|
+
forwardedIps?.[forwardedIps.length - 1] ||
|
|
19
|
+
req.socket?.remoteAddress;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
remoteAddress = req.socket?.remoteAddress;
|
|
23
|
+
}
|
|
19
24
|
const remotePort = req.socket?.remotePort;
|
|
20
25
|
return { remoteAddress, remotePort };
|
|
21
26
|
};
|
|
22
|
-
const { MCP_API_PATH = '/mcp'
|
|
23
|
-
const
|
|
27
|
+
const { MCP_API_PATH = '/mcp' } = process.env;
|
|
28
|
+
const createMiddlewareHook = (authConfig, trustProxy = false) => async function middlewareHook(req, reply) {
|
|
24
29
|
const setHeader = (key, value) => reply.header(key, value);
|
|
25
30
|
const getHeader = (key) => req.headers[key];
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
user._inLoginMethodResponse = true;
|
|
39
|
-
return { user, ...tokenObject };
|
|
40
|
-
};
|
|
41
|
-
const logout = async function logout() {
|
|
42
|
-
const tokenObject = {
|
|
43
|
-
_id: req.session.sessionId,
|
|
44
|
-
userId: req.session?.userId,
|
|
45
|
-
};
|
|
46
|
-
req.session.userId = null;
|
|
47
|
-
req.session.impersonatorId = null;
|
|
48
|
-
await emit(API_EVENTS.API_LOGOUT, tokenObject);
|
|
49
|
-
return true;
|
|
31
|
+
const getCookie = (name) => req.cookies?.[name];
|
|
32
|
+
const setCookie = (name, value, options) => reply.setCookie(name, value, options);
|
|
33
|
+
const clearCookie = (name, options) => reply.clearCookie(name, { ...options, maxAge: 0 });
|
|
34
|
+
const { remoteAddress, remotePort } = resolveUserRemoteAddress(req, trustProxy);
|
|
35
|
+
const authContextParams = {
|
|
36
|
+
setHeader,
|
|
37
|
+
getHeader,
|
|
38
|
+
getCookie,
|
|
39
|
+
setCookie,
|
|
40
|
+
clearCookie,
|
|
41
|
+
remoteAddress,
|
|
42
|
+
remotePort,
|
|
50
43
|
};
|
|
51
|
-
const
|
|
44
|
+
const authContext = await createAuthContext(authContextParams, authConfig);
|
|
45
|
+
const context = getCurrentContextResolver();
|
|
52
46
|
req.unchainedContext = await context({
|
|
53
47
|
setHeader,
|
|
54
48
|
getHeader,
|
|
55
49
|
remoteAddress,
|
|
56
50
|
remotePort,
|
|
57
|
-
login,
|
|
58
|
-
logout,
|
|
59
|
-
accessToken,
|
|
60
|
-
userId:
|
|
61
|
-
impersonatorId:
|
|
51
|
+
login: authContext.login,
|
|
52
|
+
logout: authContext.logout,
|
|
53
|
+
accessToken: authContext.accessToken,
|
|
54
|
+
userId: authContext.userId,
|
|
55
|
+
impersonatorId: authContext.impersonatorId,
|
|
56
|
+
tokenVersion: authContext.tokenVersion,
|
|
62
57
|
}, req, reply);
|
|
63
58
|
};
|
|
64
59
|
export const unchainedLogger = (prefix) => {
|
|
@@ -77,8 +72,12 @@ export const unchainedLogger = (prefix) => {
|
|
|
77
72
|
};
|
|
78
73
|
return new Logger();
|
|
79
74
|
};
|
|
80
|
-
export const connect = (fastify, { graphqlHandler,
|
|
75
|
+
export const connect = async (fastify, { graphqlHandler, unchainedAPI, }, { allowRemoteToLocalhostSecureCookies = false, adminUI = false, chat, authConfig, trustProxy = false, } = {}) => {
|
|
81
76
|
if (allowRemoteToLocalhostSecureCookies) {
|
|
77
|
+
if (process.env.NODE_ENV === 'production') {
|
|
78
|
+
throw new Error('allowRemoteToLocalhostSecureCookies is not allowed in production. ' +
|
|
79
|
+
'Configure a proper CORS policy with specific allowed origins instead.');
|
|
80
|
+
}
|
|
82
81
|
fastify.addHook('preHandler', async function (request) {
|
|
83
82
|
request.headers['x-forwarded-proto'] = 'https';
|
|
84
83
|
});
|
|
@@ -92,94 +91,34 @@ export const connect = (fastify, { graphqlHandler, db, unchainedAPI, }, { allowR
|
|
|
92
91
|
});
|
|
93
92
|
});
|
|
94
93
|
}
|
|
95
|
-
const cookieName = UNCHAINED_COOKIE_NAME;
|
|
96
|
-
const domain = UNCHAINED_COOKIE_DOMAIN;
|
|
97
|
-
const path = UNCHAINED_COOKIE_PATH;
|
|
98
|
-
const secure = UNCHAINED_COOKIE_INSECURE ? false : true;
|
|
99
|
-
const sameSite = ({
|
|
100
|
-
none: 'none',
|
|
101
|
-
lax: 'lax',
|
|
102
|
-
strict: 'strict',
|
|
103
|
-
'1': true,
|
|
104
|
-
'0': false,
|
|
105
|
-
}[UNCHAINED_COOKIE_SAMESITE?.trim()?.toLowerCase()] || false);
|
|
106
94
|
if (!fastify.hasPlugin('@fastify/cookie')) {
|
|
107
95
|
fastify.register(fastifyCookie);
|
|
108
96
|
}
|
|
109
|
-
fastify.register(fastifySession, {
|
|
110
|
-
secret: process.env.UNCHAINED_TOKEN_SECRET,
|
|
111
|
-
cookieName,
|
|
112
|
-
store: MongoStore.create({
|
|
113
|
-
client: db.client,
|
|
114
|
-
dbName: db.databaseName,
|
|
115
|
-
collectionName: 'sessions',
|
|
116
|
-
}),
|
|
117
|
-
cookie: {
|
|
118
|
-
domain,
|
|
119
|
-
httpOnly: true,
|
|
120
|
-
path,
|
|
121
|
-
secure,
|
|
122
|
-
sameSite,
|
|
123
|
-
maxAge: 1000 * 60 * 60 * 24 * 7,
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
97
|
fastify.decorateRequest('unchainedContext');
|
|
127
|
-
fastify.addHook('onRequest',
|
|
98
|
+
fastify.addHook('onRequest', createMiddlewareHook(authConfig, trustProxy || allowRemoteToLocalhostSecureCookies));
|
|
128
99
|
fastify.route({
|
|
129
|
-
url:
|
|
100
|
+
url: graphqlHandler.graphqlEndpoint,
|
|
130
101
|
method: ['GET', 'POST', 'OPTIONS'],
|
|
131
102
|
handler: async (req, reply) => {
|
|
132
|
-
|
|
103
|
+
return graphqlHandler.handleNodeRequestAndResponse(req, reply, {
|
|
133
104
|
req,
|
|
134
105
|
reply,
|
|
135
106
|
});
|
|
136
|
-
response.headers.forEach((value, key) => {
|
|
137
|
-
reply.header(key, value);
|
|
138
|
-
});
|
|
139
|
-
reply.status(response.status);
|
|
140
|
-
reply.send(response.body);
|
|
141
|
-
return reply;
|
|
142
107
|
},
|
|
143
108
|
});
|
|
144
|
-
fastify.route({
|
|
145
|
-
url: ERC_METADATA_API_PATH,
|
|
146
|
-
method: ['GET'],
|
|
147
|
-
handler: ercMetadataHandler,
|
|
148
|
-
});
|
|
149
109
|
fastify.route({
|
|
150
110
|
url: MCP_API_PATH,
|
|
151
111
|
method: ['GET', 'POST', 'DELETE'],
|
|
152
112
|
handler: mcpHandler,
|
|
153
113
|
});
|
|
154
|
-
fastify.register((s, opts, registered) => {
|
|
155
|
-
s.register(fastifyMultipart, { throwFileSizeLimit: true, limits: { fileSize: 1024 * 1024 * 35 } });
|
|
156
|
-
s.route({
|
|
157
|
-
url: TEMP_UPLOAD_API_PATH,
|
|
158
|
-
method: ['POST'],
|
|
159
|
-
bodyLimit: 1024 * 1024 * 35,
|
|
160
|
-
handler: tempUploadHandler,
|
|
161
|
-
});
|
|
162
|
-
registered();
|
|
163
|
-
});
|
|
164
|
-
fastify.register((s, opts, registered) => {
|
|
165
|
-
s.removeAllContentTypeParsers();
|
|
166
|
-
s.addContentTypeParser('*', function (req, payload, done) {
|
|
167
|
-
done(null);
|
|
168
|
-
});
|
|
169
|
-
s.route({
|
|
170
|
-
url: BULK_IMPORT_API_PATH,
|
|
171
|
-
method: ['POST'],
|
|
172
|
-
bodyLimit: 1024 * 1024 * 1024 * 5,
|
|
173
|
-
handler: bulkImportHandler,
|
|
174
|
-
});
|
|
175
|
-
registered();
|
|
176
|
-
});
|
|
177
114
|
if (chat) {
|
|
178
115
|
connectChat(fastify, chat);
|
|
179
116
|
}
|
|
180
|
-
|
|
181
|
-
|
|
117
|
+
const routes = pluginRegistry.getRoutes();
|
|
118
|
+
if (authConfig?.oidcProviders?.length) {
|
|
119
|
+
routes.push(createBackchannelLogoutRoute(authConfig.oidcProviders));
|
|
182
120
|
}
|
|
121
|
+
mountRoutes(fastify, unchainedAPI, routes);
|
|
183
122
|
if (adminUI) {
|
|
184
123
|
fastify.register(adminUIRouter, {
|
|
185
124
|
enabled: true,
|
|
@@ -243,5 +182,4 @@ export const adminUIRouter = async (fastify, opts) => {
|
|
|
243
182
|
}
|
|
244
183
|
}
|
|
245
184
|
};
|
|
246
|
-
export const fastifyRouter = adminUIRouter;
|
|
247
185
|
export { connectChat };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Context } from 'vm';
|
|
2
|
-
import type { LocalizationType, LocalizationModuleConfig } from './
|
|
2
|
+
import type { LocalizationType, LocalizationModuleConfig } from './schemas.ts';
|
|
3
3
|
export declare const getLocalizationsConfig: (context: Context, localizationType: LocalizationType) => LocalizationModuleConfig;
|