@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.
Files changed (53) hide show
  1. package/lib/acl.js +29 -1
  2. package/lib/api-index.d.ts +1 -0
  3. package/lib/auth.js +1 -0
  4. package/lib/context.d.ts +1 -0
  5. package/lib/context.js +8 -3
  6. package/lib/events.d.ts +2 -0
  7. package/lib/events.js +2 -0
  8. package/lib/express/index.d.ts +6 -9
  9. package/lib/express/index.js +52 -125
  10. package/lib/fastify/index.d.ts +6 -9
  11. package/lib/fastify/index.js +51 -113
  12. package/lib/mcp/tools/localization/getLocalizationsConfig.d.ts +1 -1
  13. package/lib/mcp/tools/localization/schemas.d.ts +34 -0
  14. package/lib/mcp/tools/order/handlers/getTopCustomers.d.ts +2 -0
  15. package/lib/mcp/tools/quotation/handlers/getQuotation.d.ts +2 -0
  16. package/lib/mcp/tools/quotation/handlers/listQuotations.d.ts +2 -0
  17. package/lib/mcp/tools/quotation/handlers/makeQuotationProposal.d.ts +2 -0
  18. package/lib/mcp/tools/quotation/handlers/rejectQuotation.d.ts +2 -0
  19. package/lib/mcp/tools/quotation/handlers/verifyQuotation.d.ts +2 -0
  20. package/lib/mcp/tools/system/handlers/countEvents.d.ts +8 -1
  21. package/lib/mcp/tools/system/handlers/countWork.d.ts +10 -1
  22. package/lib/mcp/tools/system/handlers/countWork.js +1 -0
  23. package/lib/mcp/tools/system/handlers/index.d.ts +4 -4
  24. package/lib/mcp/tools/system/handlers/listEvents.d.ts +12 -1
  25. package/lib/mcp/tools/system/handlers/listWork.d.ts +14 -1
  26. package/lib/mcp/tools/system/handlers/listWork.js +1 -0
  27. package/lib/mcp/tools/users/handlers/addUserEmail.d.ts +2 -0
  28. package/lib/mcp/tools/users/handlers/createUser.d.ts +2 -0
  29. package/lib/mcp/tools/users/handlers/enrollUser.d.ts +2 -0
  30. package/lib/mcp/tools/users/handlers/getCurrentUser.d.ts +2 -0
  31. package/lib/mcp/tools/users/handlers/getUser.d.ts +2 -0
  32. package/lib/mcp/tools/users/handlers/listUsers.d.ts +2 -0
  33. package/lib/mcp/tools/users/handlers/setUserTags.d.ts +2 -0
  34. package/lib/mcp/tools/users/handlers/setUserUsername.d.ts +2 -0
  35. package/lib/mcp/tools/users/handlers/updateUser.d.ts +2 -0
  36. package/lib/mcp/utils/getNormalizedQuotationDetails.d.ts +2 -0
  37. package/lib/mcp/utils/getNormalizedUserDetails.d.ts +2 -0
  38. package/lib/mcp/utils/normalizeMediaUrl.js +1 -1
  39. package/lib/mcp/utils/sanitizeLocalizationEntityData.d.ts +1 -1
  40. package/lib/mcp/utils/validateIsoCode.d.ts +1 -1
  41. package/lib/resolvers/mutations/index.d.ts +1 -6
  42. package/lib/resolvers/mutations/index.js +2 -12
  43. package/lib/resolvers/mutations/orders/updateCart.js +7 -1
  44. package/lib/resolvers/queries/orders/orders.js +2 -2
  45. package/lib/resolvers/type/index.d.ts +0 -1
  46. package/lib/resolvers/type/order/order-delivery-pickup-types.d.ts +0 -1
  47. package/lib/resolvers/type/order/order-delivery-pickup-types.js +0 -7
  48. package/lib/roles/index.js +1 -0
  49. package/lib/roles/loggedIn.js +1 -0
  50. package/lib/schema/mutation.js +6 -44
  51. package/lib/schema/types/index.js +2 -2
  52. package/lib/schema/types/order/delivery.js +0 -2
  53. 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,
@@ -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
@@ -16,6 +16,7 @@ export interface UnchainedUserContext {
16
16
  userId?: string;
17
17
  impersonatorId?: string;
18
18
  accessToken?: string;
19
+ tokenVersion?: number;
19
20
  user?: User;
20
21
  }
21
22
  export interface CustomAdminUiProperties {
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
- userContext.user = user;
25
- userContext.userId = user._id;
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
@@ -1,4 +1,6 @@
1
1
  export const API_EVENTS = {
2
2
  API_LOGIN_TOKEN_CREATED: 'API_LOGIN_TOKEN_CREATED',
3
3
  API_LOGOUT: 'API_LOGOUT',
4
+ ACL_DENIED: 'ACL_DENIED',
5
+ ACL_GRANTED_SENSITIVE: 'ACL_GRANTED_SENSITIVE',
4
6
  };
@@ -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, db, unchainedAPI, }: {
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, initPluginMiddlewares, }?: {
15
+ }, { allowRemoteToLocalhostSecureCookies, adminUI, chat, authConfig, trustProxy, }?: {
17
16
  allowRemoteToLocalhostSecureCookies?: boolean;
18
17
  adminUI?: boolean | Omit<AdminUIRouterOptions, "enabled">;
19
18
  chat?: ChatConfiguration;
20
- initPluginMiddlewares?: (app: e.Express, { unchainedAPI }: {
21
- unchainedAPI: UnchainedCore;
22
- }) => void;
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 };
@@ -1,17 +1,12 @@
1
1
  import e from 'express';
2
- import session from 'express-session';
3
- import multer from 'multer';
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 createBulkImportMiddleware from "./createBulkImportMiddleware.js";
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
- const remoteAddress = req.headers['x-real-ip'] ||
29
- req.headers['x-forwarded-for'] ||
30
- req.socket?.remoteAddress;
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 storage = multer.memoryStorage();
35
- const upload = multer({ storage: storage });
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 { remoteAddress, remotePort } = resolveUserRemoteAddress(req);
42
- const context = getCurrentContextResolver();
43
- const login = async (user, options = {}) => {
44
- const { impersonator } = options;
45
- await new Promise((resolve, reject) => {
46
- req.login(user, (error, result) => {
47
- if (error) {
48
- return reject(error);
49
- }
50
- req.session.impersonatorId = impersonator?._id;
51
- return resolve(result);
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 [, accessToken] = req.headers.authorization?.split(' ') || [];
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: req.user?._id,
107
- impersonatorId: req.session.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, db, unchainedAPI, }, { allowRemoteToLocalhostSecureCookies = false, adminUI = false, chat, initPluginMiddlewares, } = {}) => {
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
- const passport = new Passport();
129
- passport.serializeUser(function serialize(user, done) {
130
- done(null, user._id);
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
- if (initPluginMiddlewares) {
176
- initPluginMiddlewares(expressApp, { unchainedAPI });
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 };
@@ -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, db, unchainedAPI, }: {
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, initPluginMiddlewares, }?: {
15
+ }, { allowRemoteToLocalhostSecureCookies, adminUI, chat, authConfig, trustProxy, }?: {
17
16
  allowRemoteToLocalhostSecureCookies?: boolean;
18
17
  adminUI?: boolean | Omit<AdminUIRouterOptions, "enabled">;
19
18
  chat?: ChatConfiguration;
20
- initPluginMiddlewares?: (app: FastifyInstance, { unchainedAPI }: {
21
- unchainedAPI: UnchainedCore;
22
- }) => void;
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 };
@@ -1,64 +1,59 @@
1
1
  import { getCurrentContextResolver } from "../context.js";
2
- import bulkImportHandler from "./bulkImportHandler.js";
3
- import ercMetadataHandler from "./ercMetadataHandler.js";
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
- const resolveUserRemoteAddress = (req) => {
16
- const remoteAddress = req.headers['x-real-ip'] ||
17
- req.headers['x-forwarded-for'] ||
18
- req.socket?.remoteAddress;
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', GRAPHQL_API_PATH = '/graphql', BULK_IMPORT_API_PATH = '/bulk-import', TEMP_UPLOAD_API_PATH = '/temp-upload', ERC_METADATA_API_PATH = '/erc-metadata/:productId/:localeOrTokenFilename/:tokenFileName?', UNCHAINED_COOKIE_NAME = 'unchained_token', UNCHAINED_COOKIE_PATH = '/', UNCHAINED_COOKIE_DOMAIN, UNCHAINED_COOKIE_SAMESITE = 'none', UNCHAINED_COOKIE_INSECURE, } = process.env;
23
- const middlewareHook = async function middlewareHook(req, reply) {
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 { remoteAddress, remotePort } = resolveUserRemoteAddress(req);
27
- const context = getCurrentContextResolver();
28
- const login = async function (user, options = {}) {
29
- const { impersonator } = options;
30
- req.session.userId = user._id;
31
- req.session.impersonatorId = impersonator?._id;
32
- const tokenObject = {
33
- _id: req.session.sessionId,
34
- userId: user._id,
35
- tokenExpires: new Date(req.session.cookie._expires),
36
- };
37
- await emit(API_EVENTS.API_LOGIN_TOKEN_CREATED, tokenObject);
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 [, accessToken] = req.headers.authorization?.split(' ') || [];
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: req.session.userId,
61
- impersonatorId: req.session.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, db, unchainedAPI, }, { allowRemoteToLocalhostSecureCookies = false, adminUI = false, chat, initPluginMiddlewares, } = {}) => {
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', middlewareHook);
98
+ fastify.addHook('onRequest', createMiddlewareHook(authConfig, trustProxy || allowRemoteToLocalhostSecureCookies));
128
99
  fastify.route({
129
- url: GRAPHQL_API_PATH,
100
+ url: graphqlHandler.graphqlEndpoint,
130
101
  method: ['GET', 'POST', 'OPTIONS'],
131
102
  handler: async (req, reply) => {
132
- const response = await graphqlHandler.handleNodeRequestAndResponse(req, reply, {
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
- if (initPluginMiddlewares) {
181
- initPluginMiddlewares(fastify, { unchainedAPI });
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 './types.ts';
2
+ import type { LocalizationType, LocalizationModuleConfig } from './schemas.ts';
3
3
  export declare const getLocalizationsConfig: (context: Context, localizationType: LocalizationType) => LocalizationModuleConfig;