@eventcatalog/core 2.43.5 → 2.44.1

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.
@@ -37,7 +37,7 @@ var import_axios = __toESM(require("axios"), 1);
37
37
  var import_os = __toESM(require("os"), 1);
38
38
 
39
39
  // package.json
40
- var version = "2.43.5";
40
+ var version = "2.44.1";
41
41
 
42
42
  // src/constants.ts
43
43
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-FLVILMI6.js";
4
- import "../chunk-JVRNO62X.js";
3
+ } from "../chunk-AUGREOCT.js";
4
+ import "../chunk-HGLZ22GT.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -106,7 +106,7 @@ var import_axios = __toESM(require("axios"), 1);
106
106
  var import_os = __toESM(require("os"), 1);
107
107
 
108
108
  // package.json
109
- var version = "2.43.5";
109
+ var version = "2.44.1";
110
110
 
111
111
  // src/constants.ts
112
112
  var VERSION = version;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-ROUO6U5X.js";
4
- import "../chunk-FLVILMI6.js";
5
- import "../chunk-JVRNO62X.js";
3
+ } from "../chunk-ECUXDBJ7.js";
4
+ import "../chunk-AUGREOCT.js";
5
+ import "../chunk-HGLZ22GT.js";
6
6
  import "../chunk-E7TXTI7G.js";
7
7
  export {
8
8
  log_build_default as default
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-JVRNO62X.js";
3
+ } from "./chunk-HGLZ22GT.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import axios from "axios";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-FLVILMI6.js";
3
+ } from "./chunk-AUGREOCT.js";
4
4
  import {
5
5
  getEventCatalogConfigFile,
6
6
  verifyRequiredFieldsAreInCatalogConfigFile
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "2.43.5";
2
+ var version = "2.44.1";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "2.43.5";
28
+ var version = "2.44.1";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-JVRNO62X.js";
3
+ } from "./chunk-HGLZ22GT.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -157,7 +157,7 @@ var import_axios = __toESM(require("axios"), 1);
157
157
  var import_os = __toESM(require("os"), 1);
158
158
 
159
159
  // package.json
160
- var version = "2.43.5";
160
+ var version = "2.44.1";
161
161
 
162
162
  // src/constants.ts
163
163
  var VERSION = version;
@@ -6,8 +6,8 @@ import {
6
6
  } from "./chunk-DCLTVJDP.js";
7
7
  import {
8
8
  log_build_default
9
- } from "./chunk-ROUO6U5X.js";
10
- import "./chunk-FLVILMI6.js";
9
+ } from "./chunk-ECUXDBJ7.js";
10
+ import "./chunk-AUGREOCT.js";
11
11
  import {
12
12
  catalogToAstro,
13
13
  checkAndConvertMdToMdx
@@ -15,7 +15,7 @@ import {
15
15
  import "./chunk-EXAALOQA.js";
16
16
  import {
17
17
  VERSION
18
- } from "./chunk-JVRNO62X.js";
18
+ } from "./chunk-HGLZ22GT.js";
19
19
  import {
20
20
  isAuthEnabled,
21
21
  isBackstagePluginEnabled,
@@ -7,6 +7,7 @@ import { isAuthEnabled, isSSR } from '@utils/feature';
7
7
  import Google from '@auth/core/providers/google';
8
8
  import Auth0 from '@auth/core/providers/auth0';
9
9
  import Entra from '@auth/core/providers/microsoft-entra-id';
10
+ import jwt from 'jsonwebtoken';
10
11
 
11
12
  // Need to try and read the eventcatalog.auth.js file and get the auth providers from there
12
13
  const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
@@ -23,8 +24,7 @@ const getAuthProviders = async () => {
23
24
  const githubConfig = authConfig.providers.github;
24
25
  providers.push(
25
26
  GitHub({
26
- clientId: githubConfig.clientId,
27
- clientSecret: githubConfig.clientSecret,
27
+ ...githubConfig,
28
28
  })
29
29
  );
30
30
  console.log('✅ GitHub provider configured');
@@ -35,8 +35,7 @@ const getAuthProviders = async () => {
35
35
  const googleConfig = authConfig.providers.google;
36
36
  providers.push(
37
37
  Google({
38
- clientId: googleConfig.clientId,
39
- clientSecret: googleConfig.clientSecret,
38
+ ...googleConfig,
40
39
  })
41
40
  );
42
41
  console.log('✅ Google provider configured');
@@ -47,9 +46,12 @@ const getAuthProviders = async () => {
47
46
  const oktaConfig = authConfig.providers.okta;
48
47
  providers.push(
49
48
  Okta({
50
- clientId: oktaConfig.clientId,
51
- clientSecret: oktaConfig.clientSecret,
52
- issuer: oktaConfig.issuer,
49
+ authorization: {
50
+ params: {
51
+ scope: 'openid email profile groups',
52
+ },
53
+ },
54
+ ...oktaConfig,
53
55
  })
54
56
  );
55
57
  console.log('✅ Okta provider configured');
@@ -60,22 +62,27 @@ const getAuthProviders = async () => {
60
62
  const auth0Config = authConfig.providers.auth0;
61
63
  providers.push(
62
64
  Auth0({
63
- clientId: auth0Config.clientId,
64
- clientSecret: auth0Config.clientSecret,
65
- issuer: auth0Config.issuer,
65
+ authorization: {
66
+ params: {
67
+ scope: 'openid email profile groups',
68
+ },
69
+ },
70
+ ...auth0Config,
66
71
  })
67
72
  );
68
73
  console.log('✅ Auth0 provider configured');
69
74
  }
70
75
 
71
- // Microsoft Entra ID provider
72
76
  if (authConfig.providers?.entra) {
73
77
  const entraConfig = authConfig.providers.entra;
74
78
  providers.push(
75
79
  Entra({
76
- clientId: entraConfig.clientId,
77
- clientSecret: entraConfig.clientSecret,
78
- issuer: entraConfig.issuer,
80
+ authorization: {
81
+ params: {
82
+ scope: 'openid profile email',
83
+ },
84
+ },
85
+ ...entraConfig,
79
86
  })
80
87
  );
81
88
  console.log('✅ Microsoft Entra ID provider configured');
@@ -87,7 +94,6 @@ const getAuthProviders = async () => {
87
94
 
88
95
  return providers;
89
96
  } catch (error) {
90
- console.log('No eventcatalog.auth.js found or error loading config:', (error as Error).message);
91
97
  return [];
92
98
  }
93
99
  };
@@ -105,7 +111,6 @@ const getAuthConfig = async () => {
105
111
 
106
112
  // If custom auth config is specified (Enterprise feature)
107
113
  if (authConfig?.customAuthConfig) {
108
- console.log('🚀 Loading custom auth configuration:', authConfig.customAuthConfig);
109
114
  try {
110
115
  const customConfig = await import(/* @vite-ignore */ join(catalogDirectory, authConfig.customAuthConfig));
111
116
  return customConfig.default;
@@ -125,6 +130,45 @@ const getAuthConfig = async () => {
125
130
  // Just allow everyone who can authenticate with the provider
126
131
  return true;
127
132
  },
133
+ async jwt({ token, account, profile }: { token: any; account: Account | null; profile?: Profile }) {
134
+ // Persist provider info in JWT
135
+ if (account && profile) {
136
+ token.provider = account.provider;
137
+ token.login = (profile as any).login || (profile as any).preferred_username;
138
+
139
+ // Handle groups from different providers
140
+ if (account.provider === 'microsoft-entra-id') {
141
+ token.groups = (profile as any).roles || (profile as any).groups || [];
142
+ } else if (account.provider === 'okta') {
143
+ // For Okta, try profile first, then decode access token
144
+ token.groups = (profile as any).groups || [];
145
+ token.roles = (profile as any).roles || [];
146
+
147
+ // If no groups in profile, decode the access token
148
+ if ((!token.groups || token.groups.length === 0) && account.access_token) {
149
+ try {
150
+ // Import jwt at the top of your file if not already imported
151
+ const decodedAccessToken = jwt.decode(account.access_token);
152
+
153
+ if (decodedAccessToken && typeof decodedAccessToken === 'object') {
154
+ token.groups = (decodedAccessToken as any).groups || [];
155
+ token.roles = (decodedAccessToken as any).roles || [];
156
+ }
157
+ } catch (error) {
158
+ console.error('🔍 Error decoding Okta access token:', error);
159
+ }
160
+ }
161
+ } else if (account.provider === 'auth0') {
162
+ token.groups = (profile as any)['https://eventcatalog.dev/groups'] || [];
163
+ token.roles = (profile as any)['https://eventcatalog.dev/roles'] || [];
164
+ }
165
+
166
+ // Store access token for potential API calls
167
+ token.accessToken = account.access_token;
168
+ }
169
+
170
+ return token;
171
+ },
128
172
  async session({ session, token }: { session: Session; token: any }) {
129
173
  // Add provider info to session
130
174
  if (token?.provider) {
@@ -133,15 +177,23 @@ const getAuthConfig = async () => {
133
177
  if (token?.login) {
134
178
  (session.user as any).username = token.login;
135
179
  }
136
- return session;
137
- },
138
- async jwt({ token, account, profile }: { token: any; account: Account | null; profile?: Profile }) {
139
- // Persist provider info in JWT
140
- if (account && profile) {
141
- token.provider = account.provider;
142
- token.login = (profile as any).login || (profile as any).preferred_username;
180
+
181
+ // Add groups to session
182
+ if (token?.groups) {
183
+ (session.user as any).groups = token.groups;
143
184
  }
144
- return token;
185
+
186
+ // Add roles if available
187
+ if (token?.roles) {
188
+ (session.user as any).roles = token.roles;
189
+ }
190
+
191
+ // Add access token to session
192
+ if (token?.accessToken) {
193
+ (session as any).accessToken = token.accessToken;
194
+ }
195
+
196
+ return session;
145
197
  },
146
198
  },
147
199
  pages: {
@@ -103,6 +103,7 @@ if (collection === 'domain-context-map') {
103
103
  client:only="react"
104
104
  linksToVisualiser={linksToVisualiser}
105
105
  links={links}
106
+ mode={mode}
106
107
  />
107
108
  </div>
108
109
 
@@ -12,9 +12,13 @@ import {
12
12
  type Edge,
13
13
  type Node,
14
14
  useReactFlow,
15
+ getNodesBounds,
16
+ getViewportForBounds,
15
17
  } from '@xyflow/react';
16
18
  import '@xyflow/react/dist/style.css';
17
19
  import { HistoryIcon } from 'lucide-react';
20
+ import { toPng } from 'html-to-image';
21
+ import { DocumentArrowDownIcon } from '@heroicons/react/24/outline';
18
22
  // Nodes and edges
19
23
  import ServiceNode from './Nodes/Service';
20
24
  import FlowNode from './Nodes/Flow';
@@ -31,11 +35,11 @@ import CustomNode from './Nodes/Custom';
31
35
  import type { CollectionEntry } from 'astro:content';
32
36
  import { navigate } from 'astro:transitions/client';
33
37
  import type { CollectionTypes } from '@types';
34
- import DownloadButton from './DownloadButton';
35
38
  import { buildUrl } from '@utils/url-builder';
36
39
  import ChannelNode from './Nodes/Channel';
37
40
  import { CogIcon } from '@heroicons/react/20/solid';
38
41
  import { useEventCatalogVisualiser } from 'src/hooks/eventcatalog-visualizer';
42
+ import VisualiserSearch, { type VisualiserSearchRef } from './VisualiserSearch';
39
43
  interface Props {
40
44
  nodes: any;
41
45
  edges: any;
@@ -47,6 +51,7 @@ interface Props {
47
51
  includeKey?: boolean;
48
52
  linksToVisualiser?: boolean;
49
53
  links?: { label: string; url: string }[];
54
+ mode?: 'full' | 'simple';
50
55
  }
51
56
 
52
57
  const getVisualiserUrlForCollection = (collectionItem: CollectionEntry<CollectionTypes>) => {
@@ -62,6 +67,7 @@ const NodeGraphBuilder = ({
62
67
  includeKey = true,
63
68
  linksToVisualiser = false,
64
69
  links = [],
70
+ mode = 'full',
65
71
  }: Props) => {
66
72
  const nodeTypes = useMemo(
67
73
  () => ({
@@ -92,7 +98,8 @@ const NodeGraphBuilder = ({
92
98
  const [isAnimated, setIsAnimated] = useState(false);
93
99
  const [animateMessages, setAnimateMessages] = useState(false);
94
100
  const { hideChannels, toggleChannelsVisibility } = useEventCatalogVisualiser({ nodes, edges, setNodes, setEdges });
95
- const { fitView } = useReactFlow();
101
+ const { fitView, getNodes } = useReactFlow();
102
+ const searchRef = useRef<VisualiserSearchRef>(null);
96
103
 
97
104
  const resetNodesAndEdges = useCallback(() => {
98
105
  setNodes((nds) =>
@@ -200,10 +207,59 @@ const NodeGraphBuilder = ({
200
207
 
201
208
  const handlePaneClick = useCallback(() => {
202
209
  setIsSettingsOpen(false);
210
+ searchRef.current?.hideSuggestions();
203
211
  resetNodesAndEdges();
204
212
  fitView({ duration: 800 });
205
213
  }, [resetNodesAndEdges, fitView]);
206
214
 
215
+ const handleNodeSelect = useCallback(
216
+ (node: Node) => {
217
+ handleNodeClick(null, node);
218
+ },
219
+ [handleNodeClick]
220
+ );
221
+
222
+ const handleSearchClear = useCallback(() => {
223
+ resetNodesAndEdges();
224
+ fitView({ duration: 800 });
225
+ }, [resetNodesAndEdges, fitView]);
226
+
227
+ const downloadImage = useCallback((dataUrl: string, filename?: string) => {
228
+ const a = document.createElement('a');
229
+ a.setAttribute('download', `${filename || 'eventcatalog'}.png`);
230
+ a.setAttribute('href', dataUrl);
231
+ a.click();
232
+ }, []);
233
+
234
+ const handleExportVisual = useCallback(() => {
235
+ const imageWidth = 1024;
236
+ const imageHeight = 768;
237
+ const nodesBounds = getNodesBounds(getNodes());
238
+ const width = imageWidth > nodesBounds.width ? imageWidth : nodesBounds.width;
239
+ const height = imageHeight > nodesBounds.height ? imageHeight : nodesBounds.height;
240
+ const viewport = getViewportForBounds(nodesBounds, width, height, 0.5, 2, 0);
241
+
242
+ // Hide settings panel and controls during export
243
+ setIsSettingsOpen(false);
244
+ const controls = document.querySelector('.react-flow__controls') as HTMLElement;
245
+ if (controls) controls.style.display = 'none';
246
+
247
+ toPng(document.querySelector('.react-flow__viewport') as HTMLElement, {
248
+ backgroundColor: '#f1f1f1',
249
+ width,
250
+ height,
251
+ style: {
252
+ width: width.toString(),
253
+ height: height.toString(),
254
+ transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
255
+ },
256
+ }).then((dataUrl: string) => {
257
+ downloadImage(dataUrl, title);
258
+ // Restore controls
259
+ if (controls) controls.style.display = 'block';
260
+ });
261
+ }, [getNodes, downloadImage, title]);
262
+
207
263
  const handleLegendClick = useCallback(
208
264
  (collectionType: string, groupId?: string) => {
209
265
  const updatedNodes = nodes.map((node: Node<any>) => {
@@ -304,50 +360,55 @@ const NodeGraphBuilder = ({
304
360
  className="relative"
305
361
  >
306
362
  <Panel position="top-center" className="w-full pr-6 ">
307
- <div className="flex space-x-2 justify-between items-center">
308
- <div>
309
- <button
310
- onClick={() => setIsSettingsOpen(!isSettingsOpen)}
311
- className="py-2.5 px-3 bg-white rounded-md shadow-md hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
312
- aria-label="Open settings"
313
- >
314
- <CogIcon className="h-5 w-5 text-gray-600" />
315
- </button>
316
- </div>
317
- {title && (
318
- <span className="block shadow-sm bg-white text-xl z-10 text-black px-4 py-2 border-gray-200 rounded-md border opacity-80">
319
- {title}
320
- </span>
321
- )}
322
- <div className="flex justify-end space-x-2">
323
- <DownloadButton filename={title} addPadding={false} />
324
- {/* // Dropdown for links */}
325
- {links.length > 0 && (
326
- <div className="relative flex items-center -mt-1">
327
- <span className="absolute left-2 pointer-events-none flex items-center h-full">
328
- <HistoryIcon className="h-4 w-4 text-gray-600" />
329
- </span>
330
- <select
331
- value={links.find((link) => window.location.href.includes(link.url))?.url || links[0].url}
332
- onChange={(e) => navigate(e.target.value)}
333
- className="appearance-none pl-7 pr-6 py-0 text-[14px] bg-white rounded-md border border-gray-200 hover:bg-gray-100/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
334
- style={{ minWidth: 120, height: '26px' }}
335
- >
336
- {links.map((link) => (
337
- <option key={link.url} value={link.url}>
338
- {link.label}
339
- </option>
340
- ))}
341
- </select>
342
- <span className="absolute right-2 pointer-events-none">
343
- <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
344
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
345
- </svg>
346
- </span>
347
- </div>
363
+ <div className="flex space-x-2 justify-between items-center">
364
+ <div className="flex space-x-2">
365
+ <div>
366
+ <button
367
+ onClick={() => setIsSettingsOpen(!isSettingsOpen)}
368
+ className="py-2.5 px-3 bg-white rounded-md shadow-md hover:bg-purple-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
369
+ aria-label="Open settings"
370
+ >
371
+ <CogIcon className="h-5 w-5 text-gray-600" />
372
+ </button>
373
+ </div>
374
+ {title && (
375
+ <span className="block shadow-sm bg-white text-xl z-10 text-black px-4 py-1.5 border-gray-200 rounded-md border opacity-80">
376
+ {title}
377
+ </span>
348
378
  )}
349
379
  </div>
380
+ {mode === 'full' && (
381
+ <div className="flex justify-end space-x-2 w-96">
382
+ <VisualiserSearch ref={searchRef} nodes={nodes} onNodeSelect={handleNodeSelect} onClear={handleSearchClear} />
383
+ </div>
384
+ )}
350
385
  </div>
386
+ {links.length > 0 && (
387
+ <div className="flex justify-end mt-3">
388
+ <div className="relative flex items-center -mt-1">
389
+ <span className="absolute left-2 pointer-events-none flex items-center h-full">
390
+ <HistoryIcon className="h-4 w-4 text-gray-600" />
391
+ </span>
392
+ <select
393
+ value={links.find((link) => window.location.href.includes(link.url))?.url || links[0].url}
394
+ onChange={(e) => navigate(e.target.value)}
395
+ className="appearance-none pl-7 pr-6 py-0 text-[14px] bg-white rounded-md border border-gray-200 hover:bg-gray-100/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
396
+ style={{ minWidth: 120, height: '26px' }}
397
+ >
398
+ {links.map((link) => (
399
+ <option key={link.url} value={link.url}>
400
+ {link.label}
401
+ </option>
402
+ ))}
403
+ </select>
404
+ <span className="absolute right-2 pointer-events-none">
405
+ <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
406
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
407
+ </svg>
408
+ </span>
409
+ </div>
410
+ </div>
411
+ )}
351
412
  </Panel>
352
413
 
353
414
  {isSettingsOpen && (
@@ -396,6 +457,15 @@ const NodeGraphBuilder = ({
396
457
  </div>
397
458
  <p className="text-[10px] text-gray-500">Show or hide channels in the visualizer.</p>
398
459
  </div>
460
+ <div className="pt-4 border-t border-gray-200">
461
+ <button
462
+ onClick={handleExportVisual}
463
+ className="w-full flex items-center justify-center space-x-2 px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
464
+ >
465
+ <DocumentArrowDownIcon className="w-4 h-4" />
466
+ <span>Export Visual</span>
467
+ </button>
468
+ </div>
399
469
  </div>
400
470
  </div>
401
471
  )}
@@ -437,6 +507,7 @@ interface NodeGraphProps {
437
507
  footerLabel?: string;
438
508
  linksToVisualiser?: boolean;
439
509
  links?: { label: string; url: string }[];
510
+ mode?: 'full' | 'simple';
440
511
  }
441
512
 
442
513
  const NodeGraph = ({
@@ -451,6 +522,7 @@ const NodeGraph = ({
451
522
  footerLabel,
452
523
  linksToVisualiser = false,
453
524
  links = [],
525
+ mode = 'full',
454
526
  }: NodeGraphProps) => {
455
527
  const [elem, setElem] = useState(null);
456
528
  const [showFooter, setShowFooter] = useState(true);
@@ -482,6 +554,7 @@ const NodeGraph = ({
482
554
  includeKey={includeKey}
483
555
  linksToVisualiser={linksToVisualiser}
484
556
  links={links}
557
+ mode={mode}
485
558
  />
486
559
 
487
560
  {showFooter && (
@@ -0,0 +1,207 @@
1
+ import { useState, useCallback, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
2
+ import type { Node } from '@xyflow/react';
3
+
4
+ interface VisualiserSearchProps {
5
+ nodes: Node[];
6
+ onNodeSelect: (node: Node) => void;
7
+ onClear: () => void;
8
+ onPaneClick?: () => void;
9
+ }
10
+
11
+ export interface VisualiserSearchRef {
12
+ hideSuggestions: () => void;
13
+ }
14
+
15
+ const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
16
+ ({ nodes, onNodeSelect, onClear, onPaneClick }, ref) => {
17
+ const [searchQuery, setSearchQuery] = useState('');
18
+ const [filteredSuggestions, setFilteredSuggestions] = useState<Node[]>([]);
19
+ const [showSuggestions, setShowSuggestions] = useState(false);
20
+ const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
21
+ const searchInputRef = useRef<HTMLInputElement>(null);
22
+ const containerRef = useRef<HTMLDivElement>(null);
23
+
24
+ const hideSuggestions = useCallback(() => {
25
+ setShowSuggestions(false);
26
+ setSelectedSuggestionIndex(-1);
27
+ }, []);
28
+
29
+ useImperativeHandle(
30
+ ref,
31
+ () => ({
32
+ hideSuggestions,
33
+ }),
34
+ [hideSuggestions]
35
+ );
36
+
37
+ const getNodeDisplayName = useCallback((node: Node) => {
38
+ // @ts-ignore
39
+ const name = node.data?.message?.data?.name || node.data?.service?.data?.name || node.data?.name || node.id;
40
+ // @ts-ignore
41
+ const version = node.data?.message?.data?.version || node.data?.service?.data?.version || node.data?.version;
42
+ return version ? `${name} (v${version})` : name;
43
+ }, []);
44
+
45
+ const getNodeTypeColorClass = useCallback((nodeType: string) => {
46
+ const colorClasses: { [key: string]: string } = {
47
+ events: 'bg-orange-600 text-white',
48
+ services: 'bg-pink-600 text-white',
49
+ flows: 'bg-teal-600 text-white',
50
+ commands: 'bg-blue-600 text-white',
51
+ queries: 'bg-green-600 text-white',
52
+ channels: 'bg-gray-600 text-white',
53
+ externalSystem: 'bg-pink-600 text-white',
54
+ actor: 'bg-yellow-500 text-white',
55
+ step: 'bg-gray-700 text-white',
56
+ user: 'bg-yellow-500 text-white',
57
+ custom: 'bg-gray-500 text-white',
58
+ };
59
+ return colorClasses[nodeType] || 'bg-gray-100 text-gray-700';
60
+ }, []);
61
+
62
+ const handleSearchChange = useCallback(
63
+ (event: React.ChangeEvent<HTMLInputElement>) => {
64
+ const query = event.target.value;
65
+ setSearchQuery(query);
66
+
67
+ if (query.length > 0) {
68
+ const filtered = nodes.filter((node) => {
69
+ const nodeName = getNodeDisplayName(node);
70
+ return nodeName.toLowerCase().includes(query.toLowerCase());
71
+ });
72
+ setFilteredSuggestions(filtered);
73
+ setShowSuggestions(true);
74
+ setSelectedSuggestionIndex(-1);
75
+ } else {
76
+ setFilteredSuggestions(nodes);
77
+ setShowSuggestions(true);
78
+ setSelectedSuggestionIndex(-1);
79
+ }
80
+ },
81
+ [nodes, getNodeDisplayName]
82
+ );
83
+
84
+ const handleSearchFocus = useCallback(() => {
85
+ if (searchQuery.length === 0) {
86
+ setFilteredSuggestions(nodes);
87
+ }
88
+ setShowSuggestions(true);
89
+ setSelectedSuggestionIndex(-1);
90
+ }, [nodes, searchQuery]);
91
+
92
+ const handleSuggestionClick = useCallback(
93
+ (node: Node) => {
94
+ setSearchQuery(getNodeDisplayName(node));
95
+ setShowSuggestions(false);
96
+ onNodeSelect(node);
97
+ },
98
+ [onNodeSelect, getNodeDisplayName]
99
+ );
100
+
101
+ const handleSearchKeyDown = useCallback(
102
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
103
+ if (!showSuggestions || filteredSuggestions.length === 0) return;
104
+
105
+ switch (event.key) {
106
+ case 'ArrowDown':
107
+ event.preventDefault();
108
+ setSelectedSuggestionIndex((prev) => (prev < filteredSuggestions.length - 1 ? prev + 1 : 0));
109
+ break;
110
+ case 'ArrowUp':
111
+ event.preventDefault();
112
+ setSelectedSuggestionIndex((prev) => (prev > 0 ? prev - 1 : filteredSuggestions.length - 1));
113
+ break;
114
+ case 'Enter':
115
+ event.preventDefault();
116
+ if (selectedSuggestionIndex >= 0) {
117
+ handleSuggestionClick(filteredSuggestions[selectedSuggestionIndex]);
118
+ }
119
+ break;
120
+ case 'Escape':
121
+ setShowSuggestions(false);
122
+ setSelectedSuggestionIndex(-1);
123
+ break;
124
+ }
125
+ },
126
+ [showSuggestions, filteredSuggestions, selectedSuggestionIndex, handleSuggestionClick]
127
+ );
128
+
129
+ const clearSearch = useCallback(() => {
130
+ setSearchQuery('');
131
+ setShowSuggestions(false);
132
+ setFilteredSuggestions([]);
133
+ setSelectedSuggestionIndex(-1);
134
+ onClear();
135
+ if (searchInputRef.current) {
136
+ searchInputRef.current.focus();
137
+ }
138
+ }, [onClear]);
139
+
140
+ // Close suggestions when clicking outside
141
+ useEffect(() => {
142
+ const handleClickOutside = (event: MouseEvent) => {
143
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
144
+ setShowSuggestions(false);
145
+ setSelectedSuggestionIndex(-1);
146
+ }
147
+ };
148
+
149
+ document.addEventListener('mousedown', handleClickOutside);
150
+ return () => {
151
+ document.removeEventListener('mousedown', handleClickOutside);
152
+ };
153
+ }, []);
154
+
155
+ return (
156
+ <div ref={containerRef} className="w-full max-w-md mx-auto relative">
157
+ <div className="relative">
158
+ <input
159
+ ref={searchInputRef}
160
+ type="text"
161
+ placeholder="Search nodes..."
162
+ value={searchQuery}
163
+ onChange={handleSearchChange}
164
+ onKeyDown={handleSearchKeyDown}
165
+ onFocus={handleSearchFocus}
166
+ className="w-full px-4 py-2 pr-10 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
167
+ />
168
+ {searchQuery && (
169
+ <button
170
+ onClick={clearSearch}
171
+ className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
172
+ aria-label="Clear search"
173
+ >
174
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
175
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
176
+ </svg>
177
+ </button>
178
+ )}
179
+ </div>
180
+ {showSuggestions && filteredSuggestions.length > 0 && (
181
+ <div className="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-300 rounded-md shadow-lg z-50 max-h-60 overflow-y-auto">
182
+ {filteredSuggestions.map((node, index) => {
183
+ const nodeName = getNodeDisplayName(node);
184
+ const nodeType = node.type || 'unknown';
185
+ return (
186
+ <div
187
+ key={node.id}
188
+ onClick={() => handleSuggestionClick(node)}
189
+ className={`px-4 py-2 cursor-pointer flex items-center justify-between hover:bg-gray-100 ${
190
+ index === selectedSuggestionIndex ? 'bg-purple-50' : ''
191
+ }`}
192
+ >
193
+ <span className="text-sm font-medium text-gray-900">{nodeName}</span>
194
+ <span className={`text-xs capitalize px-2 py-1 rounded ${getNodeTypeColorClass(nodeType)}`}>{nodeType}</span>
195
+ </div>
196
+ );
197
+ })}
198
+ </div>
199
+ )}
200
+ </div>
201
+ );
202
+ }
203
+ );
204
+
205
+ VisualiserSearch.displayName = 'VisualiserSearch';
206
+
207
+ export default VisualiserSearch;
@@ -0,0 +1,283 @@
1
+ // src/middleware/auth.ts
2
+ import type { MiddlewareHandler } from 'astro';
3
+ import { getSession } from 'auth-astro/server';
4
+ import { isAuthEnabled } from '@utils/feature';
5
+ import jwt from 'jsonwebtoken';
6
+
7
+ // Define the types in this file
8
+ export interface NormalizedUser {
9
+ id: string;
10
+ email: string;
11
+ name?: string;
12
+ picture?: string;
13
+ roles: string[];
14
+ permissions: string[];
15
+ groups: string[];
16
+ metadata: Record<string, any>;
17
+ provider: 'auth0' | 'okta' | 'microsoft' | 'google' | 'unknown';
18
+ raw: {
19
+ user: any;
20
+ token: any;
21
+ };
22
+ }
23
+
24
+ // Type the locals object with matching utilities
25
+ interface TypedLocals {
26
+ session: any;
27
+ user: NormalizedUser;
28
+ hasRole: (role: string) => boolean;
29
+ hasPermission: (permission: string) => boolean;
30
+ hasGroup: (group: string) => boolean;
31
+ findMatchingRule: (rules: Record<string, () => boolean>, pathname: string) => (() => boolean) | null;
32
+ matchesPattern: (pattern: string, pathname: string) => boolean;
33
+ }
34
+
35
+ // Wildcard matching utilities
36
+ export function matchesPattern(pattern: string, pathname: string): boolean {
37
+ const regexPattern = pattern
38
+ .replace(/\*/g, '[^/]+') // * matches any characters except /
39
+ .replace(/\*\*/g, '.*'); // ** matches any characters including /
40
+
41
+ const regex = new RegExp(`^${regexPattern}$`);
42
+ return regex.test(pathname);
43
+ }
44
+
45
+ function calculateSpecificity(pattern: string): number {
46
+ let score = 0;
47
+ score += (pattern.length - (pattern.match(/\*/g) || []).length) * 10;
48
+ score -= (pattern.match(/\*/g) || []).length * 5;
49
+ return score;
50
+ }
51
+
52
+ export function findMatchingRule(rules: Record<string, () => boolean>, pathname: string) {
53
+ const matches = [];
54
+
55
+ for (const [pattern, rule] of Object.entries(rules)) {
56
+ if (matchesPattern(pattern, pathname)) {
57
+ matches.push({ pattern, rule, specificity: calculateSpecificity(pattern) });
58
+ }
59
+ }
60
+
61
+ // Sort by specificity (most specific first)
62
+ matches.sort((a, b) => b.specificity - a.specificity);
63
+
64
+ return matches.length > 0 ? matches[0].rule : null;
65
+ }
66
+
67
+ export const authMiddleware: MiddlewareHandler = async (context, next) => {
68
+ const { request, redirect, locals } = context;
69
+ const url = new URL(request.url);
70
+ const pathname = url.pathname;
71
+
72
+ // If auth is disabled and we are on an auth route, redirect to home
73
+ if (!isAuthEnabled() && pathname.includes('/auth')) {
74
+ return redirect('/');
75
+ }
76
+
77
+ // Auth is disabled, skip auth check
78
+ if (!isAuthEnabled()) {
79
+ return next();
80
+ }
81
+
82
+ // Skip system/browser requests
83
+ const systemRoutes = ['/.well-known/', '/favicon.ico', '/robots.txt', '/sitemap.xml', '/_astro/', '/__astro'];
84
+ const publicRoutes = ['/auth/login', '/auth/signout', '/auth/error', '/api/auth'];
85
+
86
+ if (
87
+ pathname.startsWith('/_') ||
88
+ systemRoutes.some((route) => pathname.startsWith(route)) ||
89
+ pathname.startsWith('/.well-known/') ||
90
+ publicRoutes.some((route) => pathname.startsWith(route))
91
+ ) {
92
+ return next();
93
+ }
94
+
95
+ try {
96
+ const session = await getSession(request);
97
+
98
+ if (!session) {
99
+ const callbackUrl = encodeURIComponent(pathname + url.search);
100
+ return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
101
+ }
102
+
103
+ // Normalize user data across providers
104
+ const normalizedUser = normalizeUserData(session);
105
+
106
+ // Type assertion for locals
107
+ const typedLocals = locals as TypedLocals;
108
+
109
+ // Add session and normalized user to locals
110
+ typedLocals.session = session;
111
+ typedLocals.user = normalizedUser;
112
+
113
+ // Add helper functions for customer middleware
114
+ typedLocals.hasRole = (role: string) => normalizedUser.roles.includes(role);
115
+ typedLocals.hasPermission = (permission: string) => normalizedUser.permissions.includes(permission);
116
+ typedLocals.hasGroup = (group: string) => {
117
+ return normalizedUser.groups.includes(group);
118
+ };
119
+
120
+ // Add wildcard matching utilities to locals
121
+ typedLocals.findMatchingRule = findMatchingRule;
122
+ typedLocals.matchesPattern = matchesPattern;
123
+ } catch (error) {
124
+ console.error('Session error:', error);
125
+ const callbackUrl = encodeURIComponent(pathname + url.search);
126
+ return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
127
+ }
128
+
129
+ return next();
130
+ };
131
+
132
+ // Normalize user data from different providers
133
+ function normalizeUserData(session: any): NormalizedUser {
134
+ const user = session.user;
135
+ const accessToken = session.accessToken;
136
+ let decodedToken = null;
137
+
138
+ if (accessToken) {
139
+ try {
140
+ decodedToken = jwt.decode(accessToken);
141
+ } catch (e) {
142
+ console.warn('Could not decode access token');
143
+ }
144
+ }
145
+
146
+ const provider = detectProvider(session);
147
+
148
+ switch (provider) {
149
+ case 'auth0':
150
+ return normalizeAuth0User(user, decodedToken);
151
+ case 'okta':
152
+ return normalizeOktaUser(user, decodedToken);
153
+ case 'microsoft':
154
+ return normalizeMicrosoftUser(user, decodedToken);
155
+ case 'google':
156
+ return normalizeGoogleUser(user, decodedToken);
157
+ default:
158
+ return normalizeGenericUser(user, decodedToken);
159
+ }
160
+ }
161
+
162
+ function detectProvider(session: any): string {
163
+ const account = session.account;
164
+ if (account?.provider) return account.provider;
165
+
166
+ if (session.user?.sub?.startsWith('auth0|')) return 'auth0';
167
+ if (session.user?.iss?.includes('okta.com') || session.user?.provider?.includes('okta')) return 'okta';
168
+ if (session.user?.iss?.includes('microsoft')) return 'microsoft';
169
+ if (session.user?.iss?.includes('google')) return 'google';
170
+
171
+ return 'unknown';
172
+ }
173
+
174
+ function normalizeAuth0User(user: any, token: any): NormalizedUser {
175
+ return {
176
+ id: user.sub || user.id,
177
+ email: user.email,
178
+ name: user.name,
179
+ picture: user.picture,
180
+ roles: token?.['https://eventcatalog.dev/roles'] || [],
181
+ permissions: token?.permissions || [],
182
+ groups: token?.['https://eventcatalog.dev/groups'] || [],
183
+ metadata: token?.['https://eventcatalog.dev/app_metadata'] || {},
184
+ provider: 'auth0',
185
+ raw: { user, token },
186
+ };
187
+ }
188
+
189
+ function normalizeOktaUser(user: any, token: any): NormalizedUser {
190
+ // Try to get groups from multiple sources
191
+ let groups: string[] = [];
192
+ let roles: string[] = [];
193
+
194
+ // Source 1: User object (from session)
195
+ if (user.groups && Array.isArray(user.groups)) {
196
+ groups = user.groups;
197
+ }
198
+
199
+ // Source 2: Decoded token (from access token)
200
+ if ((!groups || groups.length === 0) && token?.groups && Array.isArray(token.groups)) {
201
+ groups = token.groups;
202
+ }
203
+
204
+ // Source 3: Roles
205
+ if (user.roles && Array.isArray(user.roles)) {
206
+ roles = user.roles;
207
+ } else if (token?.roles && Array.isArray(token.roles)) {
208
+ roles = token.roles;
209
+ }
210
+
211
+ return {
212
+ id: user.sub || user.id,
213
+ email: user.email,
214
+ name: user.name,
215
+ picture: user.picture,
216
+ roles: roles,
217
+ permissions: [],
218
+ groups: groups,
219
+ metadata: {
220
+ department: token?.department || user.department,
221
+ title: token?.title || user.title,
222
+ locale: token?.locale || user.locale,
223
+ },
224
+ provider: 'okta',
225
+ raw: { user, token },
226
+ };
227
+ }
228
+
229
+ function normalizeMicrosoftUser(user: any, token: any): NormalizedUser {
230
+ // Groups should now be available directly on the user object from the session callback
231
+ const groups = user.groups || token?.groups || [];
232
+ const roles = user.roles || token?.roles || [];
233
+
234
+ return {
235
+ id: user.sub || user.oid,
236
+ email: user.email || user.preferred_username,
237
+ name: user.name,
238
+ picture: user.picture,
239
+ roles: roles,
240
+ permissions: [],
241
+ groups: groups,
242
+ metadata: {
243
+ department: token?.extension_Department,
244
+ jobTitle: token?.jobTitle,
245
+ companyName: token?.companyName,
246
+ },
247
+ provider: 'microsoft',
248
+ raw: { user, token },
249
+ };
250
+ }
251
+
252
+ function normalizeGoogleUser(user: any, token: any): NormalizedUser {
253
+ return {
254
+ id: user.sub || user.id,
255
+ email: user.email,
256
+ name: user.name,
257
+ picture: user.picture,
258
+ roles: [],
259
+ permissions: [],
260
+ groups: [],
261
+ metadata: {
262
+ domain: token?.hd,
263
+ locale: token?.locale,
264
+ },
265
+ provider: 'google',
266
+ raw: { user, token },
267
+ };
268
+ }
269
+
270
+ function normalizeGenericUser(user: any, token: any): NormalizedUser {
271
+ return {
272
+ id: user.sub || user.id || user.email,
273
+ email: user.email,
274
+ name: user.name,
275
+ picture: user.picture,
276
+ roles: user.roles || token?.roles || [],
277
+ permissions: user.permissions || token?.permissions || [],
278
+ groups: user.groups || token?.groups || [],
279
+ metadata: {},
280
+ provider: 'unknown',
281
+ raw: { user, token },
282
+ };
283
+ }
@@ -1,62 +1,47 @@
1
- // src/middleware.ts
2
1
  import type { MiddlewareHandler } from 'astro';
3
- import { getSession } from 'auth-astro/server';
4
- import { isAuthEnabled } from '@utils/feature';
5
-
6
- export const onRequest: MiddlewareHandler = async (context, next) => {
7
- const { request, redirect, locals } = context;
8
- const url = new URL(request.url);
9
- const pathname = url.pathname;
10
-
11
- // If auth is disabled and we are on an auth route, redirect to home
12
- if (!isAuthEnabled() && pathname.includes('/auth')) {
13
- return redirect('/');
14
- }
15
-
16
- // Auth is disabled, skip auth check
17
- if (!isAuthEnabled()) {
18
- return next();
2
+ import { authMiddleware } from './middleware-auth.ts';
3
+ import { sequence } from 'astro:middleware';
4
+ import { join } from 'node:path';
5
+ import { isSSR } from '@utils/feature';
6
+
7
+ // Try to load customer's custom RBAC middleware
8
+ let customerRbacMiddleware: MiddlewareHandler | null = null;
9
+
10
+ try {
11
+ const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
12
+ const customerMiddleware = await import(/* @vite-ignore */ join(catalogDirectory, 'middleware.ts'));
13
+ customerRbacMiddleware = customerMiddleware.rbacMiddleware;
14
+
15
+ if (!isSSR()) {
16
+ // Tell user they need to build in SSR mode
17
+ console.log(
18
+ '🔴 Found custom middleware.ts file. To use RBAC, you need to build in SSR mode, by setting output: "server" in your eventcatalog.config.js file.'
19
+ );
20
+ } else {
21
+ console.log('✅ Loaded custom RBAC middleware');
19
22
  }
23
+ } catch (error) {
24
+ console.log('Error loading custom RBAC middleware:', error);
25
+ // Just silently fail, we don't want to block the app
26
+ }
20
27
 
21
- // Skip system/browser requests
22
- const systemRoutes = ['/.well-known/', '/favicon.ico', '/robots.txt', '/sitemap.xml', '/_astro/', '/__astro'];
28
+ const errorHandlingMiddleware: MiddlewareHandler = async (context, next) => {
29
+ const response = await next();
23
30
 
24
- // Skip auth check for these routes
25
- const publicRoutes = ['/auth/login', '/auth/signout', '/auth/error', '/api/auth'];
31
+ if (response.status === 403) {
32
+ const params = new URLSearchParams({
33
+ path: context.url.pathname,
34
+ returnTo: context.url.pathname + context.url.search,
35
+ });
26
36
 
27
- // Skip static files, system routes, and browser requests
28
- if (
29
- pathname.startsWith('/_') ||
30
- systemRoutes.some((route) => pathname.startsWith(route)) ||
31
- pathname.startsWith('/.well-known/')
32
- ) {
33
- return next();
37
+ return context.redirect(`/unauthorized?${params.toString()}`);
34
38
  }
35
39
 
36
- // Skip public routes
37
- if (publicRoutes.some((route) => pathname.startsWith(route))) {
38
- return next();
39
- }
40
+ return response;
41
+ };
40
42
 
41
- try {
42
- // Check if user is logged in
43
- const session = await getSession(request);
44
-
45
- if (!session) {
46
- const callbackUrl = encodeURIComponent(pathname + url.search);
47
- return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
48
- }
49
-
50
- // Add session to locals for pages to use
51
- // @ts-ignore
52
- locals.session = session;
53
- // @ts-ignore
54
- locals.user = session.user;
55
- } catch (error) {
56
- console.error('Session error:', error);
57
- const callbackUrl = encodeURIComponent(pathname + url.search);
58
- return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
59
- }
43
+ const middlewareChain = customerRbacMiddleware
44
+ ? [errorHandlingMiddleware, authMiddleware, customerRbacMiddleware]
45
+ : [errorHandlingMiddleware, authMiddleware];
60
46
 
61
- return next();
62
- };
47
+ export const onRequest = isSSR() ? sequence(...middlewareChain) : undefined;
@@ -1,11 +1,9 @@
1
1
  ---
2
2
  import config from '@config';
3
3
  const { title, logo } = config;
4
- import { getSession } from 'auth-astro/server';
5
4
  import { join } from 'node:path';
6
5
  import { isAuthEnabled, isSSR } from '@utils/feature';
7
6
 
8
- const session = await getSession(Astro.request);
9
7
  const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
10
8
 
11
9
  let hasAuthConfigurationFile = false;
@@ -25,10 +23,20 @@ const shouldShowLogin = hasAuthConfigurationFile && isSSR() && isAuthEnabled() &
25
23
  // Check if configuration exists but no providers are set up
26
24
  const hasConfigButNoProviders = hasAuthConfigurationFile && isSSR() && isAuthEnabled() && providers.length === 0;
27
25
 
28
- if (session) {
26
+ // If we are not in SSR mode, redirect to home
27
+ if (!isSSR() || !isAuthEnabled()) {
29
28
  return Astro.redirect('/');
30
29
  }
31
30
 
31
+ // If we are in SSR mode, check if the user is already logged in
32
+ if (isSSR() && isAuthEnabled()) {
33
+ const { getSession } = await import(/* @vite-ignore */ 'auth-astro/server');
34
+ const session = await getSession(Astro.request);
35
+ if (session) {
36
+ return Astro.redirect('/');
37
+ }
38
+ }
39
+
32
40
  // Provider configurations
33
41
  const providerConfig = {
34
42
  github: {
@@ -0,0 +1,84 @@
1
+ ---
2
+ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
3
+ ---
4
+
5
+ <VerticalSideBarLayout title="Unauthorized - EventCatalog">
6
+ <div class="flex h-[calc(100vh-120px)] bg-white">
7
+ <main class="flex w-full items-center justify-center bg-white text-center sm:p-12">
8
+ <div class="w-full max-w-xl text-center">
9
+ <div class="mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-red-100">
10
+ <svg
11
+ class="h-9 w-9 text-red-600"
12
+ viewBox="0 0 24 24"
13
+ fill="none"
14
+ stroke="currentColor"
15
+ stroke-width="2"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ >
19
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
20
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
21
+ </svg>
22
+ </div>
23
+
24
+ <h1 class="text-3xl font-bold text-gray-800 mb-4">Access Denied</h1>
25
+
26
+ <p class="max-w-md mx-auto text-base text-gray-500 mb-8">You don't have permission to access this resource.</p>
27
+
28
+ <!-- Client-side path display -->
29
+ <div
30
+ id="path-container"
31
+ class="hidden mb-8 w-full max-w-lg mx-auto rounded-lg border border-gray-200 bg-gray-100 p-4 text-left"
32
+ >
33
+ <p class="mb-2 text-sm font-medium text-gray-600">Requested path:</p>
34
+ <code id="requested-path" class="break-all font-mono text-sm text-gray-800"></code>
35
+ </div>
36
+
37
+ <div class="flex flex-col sm:flex-row gap-4 justify-center">
38
+ <button
39
+ onclick="history.back()"
40
+ class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
41
+ >
42
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
44
+ </svg>
45
+ Go Back
46
+ </button>
47
+
48
+ <a
49
+ href="/dashboard"
50
+ class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
51
+ >
52
+ <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
+ <path
54
+ stroke-linecap="round"
55
+ stroke-linejoin="round"
56
+ stroke-width="2"
57
+ d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
58
+ </svg>
59
+ Go to Dashboard
60
+ </a>
61
+ </div>
62
+ </div>
63
+ </main>
64
+ </div>
65
+
66
+ <!-- Client-side script -->
67
+ <script>
68
+ document.addEventListener('DOMContentLoaded', () => {
69
+ // Get query parameters on the client side
70
+ const url = new URL(window.location.href);
71
+ const path = url.searchParams.get('path');
72
+
73
+ if (path) {
74
+ const pathContainer = document.getElementById('path-container');
75
+ const requestedPath = document.getElementById('requested-path');
76
+
77
+ if (pathContainer && requestedPath) {
78
+ requestedPath.textContent = path;
79
+ pathContainer.classList.remove('hidden');
80
+ }
81
+ }
82
+ });
83
+ </script>
84
+ </VerticalSideBarLayout>
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/event-catalog/eventcatalog.git"
7
7
  },
8
8
  "type": "module",
9
- "version": "2.43.5",
9
+ "version": "2.44.1",
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
@@ -74,6 +74,7 @@
74
74
  "gray-matter": "^4.0.3",
75
75
  "html-to-image": "^1.11.11",
76
76
  "js-yaml": "^4.1.0",
77
+ "jsonwebtoken": "^9.0.2",
77
78
  "langchain": "^0.3.19",
78
79
  "lodash.debounce": "^4.0.8",
79
80
  "lodash.merge": "4.6.2",
@@ -104,6 +105,7 @@
104
105
  "@types/dagre": "^0.7.52",
105
106
  "@types/diff": "^5.2.2",
106
107
  "@types/js-yaml": "^4.0.9",
108
+ "@types/jsonwebtoken": "^9.0.10",
107
109
  "@types/lodash.debounce": "^4.0.9",
108
110
  "@types/lodash.merge": "4.6.9",
109
111
  "@types/node": "^20.14.2",