@unchainedshop/api 4.6.1 → 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.d.ts +44 -0
- package/lib/auth.js +187 -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/express/mountPluginRoutes.d.ts +3 -0
- package/lib/express/mountPluginRoutes.js +59 -0
- package/lib/express/mountRoutes.d.ts +3 -0
- package/lib/express/mountRoutes.js +57 -0
- package/lib/fastify/index.d.ts +6 -9
- package/lib/fastify/index.js +51 -113
- package/lib/fastify/mountPluginRoutes.d.ts +3 -0
- package/lib/fastify/mountPluginRoutes.js +62 -0
- package/lib/fastify/mountRoutes.d.ts +3 -0
- package/lib/fastify/mountRoutes.js +60 -0
- package/lib/handlers/createBackchannelLogoutHandler.d.ts +4 -0
- package/lib/handlers/createBackchannelLogoutHandler.js +170 -0
- 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/middleware/createAuthMiddleware.d.ts +29 -0
- package/lib/middleware/createAuthMiddleware.js +128 -0
- package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +4 -0
- package/lib/resolvers/mutations/accounts/logoutAllSessions.js +11 -0
- 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/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/resolvers/type/order/order-payment-base.d.ts +7 -0
- package/lib/resolvers/type/order/order-payment-base.js +14 -0
- 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/common.d.ts +2 -0
- package/lib/schema/types/common.js +12 -0
- package/lib/schema/types/index.js +2 -2
- package/lib/schema/types/order/delivery.js +0 -2
- package/lib/utils/mapServiceError.d.ts +1 -0
- package/lib/utils/mapServiceError.js +59 -0
- package/lib/utils/maskError.d.ts +1 -0
- package/lib/utils/maskError.js +22 -0
- package/package.json +8 -17
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createServerAdapter } from '@whatwg-node/server';
|
|
2
|
+
import { createLogger } from '@unchainedshop/logger';
|
|
3
|
+
const logger = createLogger('express');
|
|
4
|
+
export function mountRoutes(app, unchainedAPI, routes) {
|
|
5
|
+
if (routes.length === 0)
|
|
6
|
+
return;
|
|
7
|
+
const endpoints = routes.map((r) => `${r.method} ${r.path}`).join(', ');
|
|
8
|
+
logger.info(`Mounting ${routes.length} route(s): ${endpoints}`);
|
|
9
|
+
for (const route of routes) {
|
|
10
|
+
const adapter = createServerAdapter(async (request, serverContext) => {
|
|
11
|
+
const context = {
|
|
12
|
+
...unchainedAPI,
|
|
13
|
+
...serverContext.unchainedContext,
|
|
14
|
+
params: serverContext.params || {},
|
|
15
|
+
rawRequest: serverContext.rawRequest,
|
|
16
|
+
};
|
|
17
|
+
try {
|
|
18
|
+
return await route.handler(request, context);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
logger.error(`Error in route handler ${route.method} ${route.path}`, {
|
|
22
|
+
error: error instanceof Error ? error.message : String(error),
|
|
23
|
+
});
|
|
24
|
+
return new Response(JSON.stringify({
|
|
25
|
+
error: error instanceof Error ? error.message : 'Internal Server Error',
|
|
26
|
+
}), {
|
|
27
|
+
status: 500,
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const method = route.method.toLowerCase();
|
|
33
|
+
const expressHandler = async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
await adapter.handleNodeRequestAndResponse(req, res, {
|
|
36
|
+
unchainedContext: req.unchainedContext,
|
|
37
|
+
params: req.params,
|
|
38
|
+
rawRequest: req,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
logger.error(`Error handling request for ${route.method} ${route.path}`, {
|
|
43
|
+
error: error instanceof Error ? error.message : String(error),
|
|
44
|
+
});
|
|
45
|
+
if (!res.headersSent) {
|
|
46
|
+
res.status(500).json({ error: 'Internal Server Error' });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
if (method === 'all') {
|
|
51
|
+
app.use(route.path, expressHandler);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
app[method](route.path, expressHandler);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
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 };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { pluginRegistry } from '@unchainedshop/core';
|
|
2
|
+
import { createServerAdapter } from '@whatwg-node/server';
|
|
3
|
+
export function mountPluginRoutes(fastify, unchainedAPI) {
|
|
4
|
+
const routes = pluginRegistry.getRoutes();
|
|
5
|
+
if (routes.length === 0)
|
|
6
|
+
return;
|
|
7
|
+
const endpoints = routes.map((r) => `${r.method} ${r.path}`).join(', ');
|
|
8
|
+
fastify.log.info(`Mounting ${routes.length} plugin route(s): ${endpoints}`);
|
|
9
|
+
fastify.register((scope, opts, registered) => {
|
|
10
|
+
scope.removeAllContentTypeParsers();
|
|
11
|
+
scope.addContentTypeParser('*', function (request, payload, done) {
|
|
12
|
+
done(null);
|
|
13
|
+
});
|
|
14
|
+
for (const route of routes) {
|
|
15
|
+
const adapter = createServerAdapter(async (request, serverContext) => {
|
|
16
|
+
const context = {
|
|
17
|
+
...unchainedAPI,
|
|
18
|
+
...serverContext.unchainedContext,
|
|
19
|
+
params: serverContext.params || {},
|
|
20
|
+
rawRequest: serverContext.rawRequest,
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
return await route.handler(request, context);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
fastify.log.error(`Error in plugin route handler ${route.method} ${route.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
27
|
+
return new Response(JSON.stringify({
|
|
28
|
+
error: error instanceof Error ? error.message : 'Internal Server Error',
|
|
29
|
+
}), {
|
|
30
|
+
status: 500,
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
let methods;
|
|
36
|
+
if (route.method === 'ALL') {
|
|
37
|
+
methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
methods = [route.method];
|
|
41
|
+
}
|
|
42
|
+
scope.route({
|
|
43
|
+
url: route.path,
|
|
44
|
+
method: methods,
|
|
45
|
+
handler: async (req, reply) => {
|
|
46
|
+
const response = await adapter.handleNodeRequestAndResponse(req.raw, reply.raw, {
|
|
47
|
+
unchainedContext: req.unchainedContext,
|
|
48
|
+
params: req.params,
|
|
49
|
+
rawRequest: req.raw,
|
|
50
|
+
});
|
|
51
|
+
response.headers.forEach((value, key) => {
|
|
52
|
+
reply.header(key, value);
|
|
53
|
+
});
|
|
54
|
+
reply.status(response.status);
|
|
55
|
+
reply.send(response.body || undefined);
|
|
56
|
+
return reply;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
registered();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createServerAdapter } from '@whatwg-node/server';
|
|
2
|
+
export function mountRoutes(fastify, unchainedAPI, routes) {
|
|
3
|
+
if (routes.length === 0)
|
|
4
|
+
return;
|
|
5
|
+
const endpoints = routes.map((r) => `${r.method} ${r.path}`).join(', ');
|
|
6
|
+
fastify.log.info(`Mounting ${routes.length} route(s): ${endpoints}`);
|
|
7
|
+
fastify.register((scope, opts, registered) => {
|
|
8
|
+
scope.removeAllContentTypeParsers();
|
|
9
|
+
scope.addContentTypeParser('*', function (request, payload, done) {
|
|
10
|
+
done(null);
|
|
11
|
+
});
|
|
12
|
+
for (const route of routes) {
|
|
13
|
+
const adapter = createServerAdapter(async (request, serverContext) => {
|
|
14
|
+
const context = {
|
|
15
|
+
...unchainedAPI,
|
|
16
|
+
...serverContext.unchainedContext,
|
|
17
|
+
params: serverContext.params || {},
|
|
18
|
+
rawRequest: serverContext.rawRequest,
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
return await route.handler(request, context);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
fastify.log.error(`Error in route handler ${route.method} ${route.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
25
|
+
return new Response(JSON.stringify({
|
|
26
|
+
error: error instanceof Error ? error.message : 'Internal Server Error',
|
|
27
|
+
}), {
|
|
28
|
+
status: 500,
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
let methods;
|
|
34
|
+
if (route.method === 'ALL') {
|
|
35
|
+
methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
methods = [route.method];
|
|
39
|
+
}
|
|
40
|
+
scope.route({
|
|
41
|
+
url: route.path,
|
|
42
|
+
method: methods,
|
|
43
|
+
handler: async (req, reply) => {
|
|
44
|
+
const response = await adapter.handleNodeRequestAndResponse(req.raw, reply.raw, {
|
|
45
|
+
unchainedContext: req.unchainedContext,
|
|
46
|
+
params: req.params,
|
|
47
|
+
rawRequest: req.raw,
|
|
48
|
+
});
|
|
49
|
+
response.headers.forEach((value, key) => {
|
|
50
|
+
reply.header(key, value);
|
|
51
|
+
});
|
|
52
|
+
reply.status(response.status);
|
|
53
|
+
reply.send(response.body || undefined);
|
|
54
|
+
return reply;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
registered();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as jose from 'jose';
|
|
2
|
+
import { createLogger } from '@unchainedshop/logger';
|
|
3
|
+
const logger = createLogger('unchained:api:backchannel-logout');
|
|
4
|
+
function normalizeIssuer(url) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = new URL(url);
|
|
7
|
+
return parsed.origin + parsed.pathname.replace(/\/$/, '');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return url;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const jwksCache = new Map();
|
|
14
|
+
function getJWKS(jwksUri) {
|
|
15
|
+
let jwks = jwksCache.get(jwksUri);
|
|
16
|
+
if (!jwks) {
|
|
17
|
+
jwks = jose.createRemoteJWKSet(new URL(jwksUri), {
|
|
18
|
+
cooldownDuration: 30000,
|
|
19
|
+
cacheMaxAge: 600000,
|
|
20
|
+
});
|
|
21
|
+
jwksCache.set(jwksUri, jwks);
|
|
22
|
+
}
|
|
23
|
+
return jwks;
|
|
24
|
+
}
|
|
25
|
+
export function createBackchannelLogoutRoute(providers) {
|
|
26
|
+
return {
|
|
27
|
+
path: '/backchannel-logout',
|
|
28
|
+
method: 'ALL',
|
|
29
|
+
handler: async (request, context) => {
|
|
30
|
+
if (request.method !== 'POST') {
|
|
31
|
+
return new Response(JSON.stringify({ error: 'method_not_allowed' }), {
|
|
32
|
+
status: 405,
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
let logoutToken = null;
|
|
38
|
+
const contentType = request.headers.get('content-type') || '';
|
|
39
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
40
|
+
const formData = await request.formData();
|
|
41
|
+
logoutToken = formData.get('logout_token');
|
|
42
|
+
}
|
|
43
|
+
else if (contentType.includes('application/json')) {
|
|
44
|
+
const body = (await request.json());
|
|
45
|
+
logoutToken = body.logout_token || null;
|
|
46
|
+
}
|
|
47
|
+
if (!logoutToken) {
|
|
48
|
+
logger.warn('Back-channel logout request missing logout_token');
|
|
49
|
+
return new Response(JSON.stringify({ error: 'missing_logout_token' }), {
|
|
50
|
+
status: 400,
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
let decodedPayload;
|
|
55
|
+
try {
|
|
56
|
+
const parts = logoutToken.split('.');
|
|
57
|
+
if (parts.length !== 3) {
|
|
58
|
+
logger.warn('Invalid logout token format: not a valid JWT');
|
|
59
|
+
return new Response(JSON.stringify({ error: 'invalid_token' }), {
|
|
60
|
+
status: 400,
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
decodedPayload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
logger.warn('Failed to decode logout token');
|
|
68
|
+
return new Response(JSON.stringify({ error: 'invalid_token' }), {
|
|
69
|
+
status: 400,
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const { iss } = decodedPayload;
|
|
74
|
+
if (!iss || typeof iss !== 'string') {
|
|
75
|
+
logger.warn('Logout token missing issuer (iss)');
|
|
76
|
+
return new Response(JSON.stringify({ error: 'invalid_token' }), {
|
|
77
|
+
status: 400,
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const normalizedIss = normalizeIssuer(iss);
|
|
82
|
+
const provider = providers.find((p) => normalizeIssuer(p.issuer) === normalizedIss);
|
|
83
|
+
if (!provider) {
|
|
84
|
+
logger.warn('Unknown issuer in logout token:', { iss });
|
|
85
|
+
return new Response(JSON.stringify({ error: 'unknown_issuer' }), {
|
|
86
|
+
status: 400,
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const jwksUri = provider.jwksUri || `${provider.issuer}/.well-known/jwks.json`;
|
|
91
|
+
let verifiedPayload;
|
|
92
|
+
try {
|
|
93
|
+
const JWKS = getJWKS(jwksUri);
|
|
94
|
+
const verifyOptions = {
|
|
95
|
+
issuer: provider.issuer,
|
|
96
|
+
};
|
|
97
|
+
if (provider.audience) {
|
|
98
|
+
verifyOptions.audience = provider.audience;
|
|
99
|
+
}
|
|
100
|
+
const { payload } = await jose.jwtVerify(logoutToken, JWKS, verifyOptions);
|
|
101
|
+
verifiedPayload = payload;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof jose.errors.JWTExpired) {
|
|
105
|
+
logger.warn('Logout token expired');
|
|
106
|
+
return new Response(JSON.stringify({ error: 'token_expired' }), {
|
|
107
|
+
status: 400,
|
|
108
|
+
headers: { 'Content-Type': 'application/json' },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else if (error instanceof jose.errors.JWTClaimValidationFailed) {
|
|
112
|
+
logger.warn('Logout token claim validation failed:', { message: error.message });
|
|
113
|
+
return new Response(JSON.stringify({ error: 'invalid_claims' }), {
|
|
114
|
+
status: 400,
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else if (error instanceof jose.errors.JWSSignatureVerificationFailed) {
|
|
119
|
+
logger.warn('Logout token signature verification failed - possible forgery attempt');
|
|
120
|
+
return new Response(JSON.stringify({ error: 'invalid_signature' }), {
|
|
121
|
+
status: 400,
|
|
122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
logger.error('Logout token verification failed:', { error });
|
|
127
|
+
return new Response(JSON.stringify({ error: 'verification_failed' }), {
|
|
128
|
+
status: 400,
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const { sub, events } = verifiedPayload;
|
|
134
|
+
if (!events?.['http://schemas.openid.net/event/backchannel-logout']) {
|
|
135
|
+
logger.warn('Token is not a back-channel logout token (missing events claim)');
|
|
136
|
+
return new Response(JSON.stringify({ error: 'invalid_token_type' }), {
|
|
137
|
+
status: 400,
|
|
138
|
+
headers: { 'Content-Type': 'application/json' },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (!sub) {
|
|
142
|
+
logger.warn('Logout token missing subject (sub)');
|
|
143
|
+
return new Response(JSON.stringify({ error: 'missing_subject' }), {
|
|
144
|
+
status: 400,
|
|
145
|
+
headers: { 'Content-Type': 'application/json' },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const user = await context.modules.users.findUserById(sub);
|
|
149
|
+
if (!user) {
|
|
150
|
+
logger.info('User not found for back-channel logout:', { sub });
|
|
151
|
+
return new Response('', { status: 200 });
|
|
152
|
+
}
|
|
153
|
+
await context.modules.users.updateOidcLogoutAt(user._id, new Date());
|
|
154
|
+
logger.info('Back-channel logout processed successfully:', {
|
|
155
|
+
userId: user._id,
|
|
156
|
+
issuer: iss,
|
|
157
|
+
});
|
|
158
|
+
return new Response('', { status: 200 });
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
logger.error('Back-channel logout error:', { error });
|
|
162
|
+
return new Response(JSON.stringify({ error: 'internal_error' }), {
|
|
163
|
+
status: 500,
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export default createBackchannelLogoutRoute;
|
|
@@ -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;
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { SortDirection } from '@unchainedshop/utils';
|
|
3
|
+
export type LocalizationType = 'COUNTRY' | 'CURRENCY' | 'LANGUAGE';
|
|
4
|
+
export interface LocalizationModuleConfig {
|
|
5
|
+
module: any;
|
|
6
|
+
NotFoundError: any;
|
|
7
|
+
entityName: string;
|
|
8
|
+
idField: string;
|
|
9
|
+
existsMethod: any;
|
|
10
|
+
findMethod: any;
|
|
11
|
+
findMultipleMethod: any;
|
|
12
|
+
}
|
|
13
|
+
export interface LocalizationEntity {
|
|
14
|
+
isoCode: string;
|
|
15
|
+
contractAddress?: string;
|
|
16
|
+
decimals?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface LocalizationUpdateEntity {
|
|
19
|
+
isoCode?: string;
|
|
20
|
+
contractAddress?: string;
|
|
21
|
+
decimals?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface LocalizationListOptions {
|
|
24
|
+
limit?: number;
|
|
25
|
+
offset?: number;
|
|
26
|
+
includeInactive?: boolean;
|
|
27
|
+
queryString?: string;
|
|
28
|
+
sort?: {
|
|
29
|
+
key: string;
|
|
30
|
+
value: 'ASC' | 'DESC';
|
|
31
|
+
}[];
|
|
32
|
+
}
|
|
33
|
+
export interface LocalizationCountOptions {
|
|
34
|
+
includeInactive?: boolean;
|
|
35
|
+
queryString?: string;
|
|
36
|
+
}
|
|
3
37
|
export declare const sortDirectionKeys: [keyof typeof SortDirection, ...(keyof typeof SortDirection)[]];
|
|
4
38
|
export declare const LocalizationTypeEnum: z.ZodEnum<{
|
|
5
39
|
COUNTRY: "COUNTRY";
|