@eventcatalog/core 2.43.5 → 2.44.0
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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-JVRNO62X.js → chunk-C4NENBPQ.js} +1 -1
- package/dist/{chunk-ROUO6U5X.js → chunk-MF3FQSRF.js} +1 -1
- package/dist/{chunk-FLVILMI6.js → chunk-NUHIER2H.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +3 -3
- package/eventcatalog/auth.config.ts +76 -24
- package/eventcatalog/src/middleware-auth.ts +283 -0
- package/eventcatalog/src/middleware.ts +38 -53
- package/eventcatalog/src/pages/auth/login.astro +11 -3
- package/eventcatalog/src/pages/unauthorized/index.astro +84 -0
- package/package.json +3 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-MF3FQSRF.js";
|
|
4
|
+
import "../chunk-NUHIER2H.js";
|
|
5
|
+
import "../chunk-C4NENBPQ.js";
|
|
6
6
|
import "../chunk-E7TXTI7G.js";
|
|
7
7
|
export {
|
|
8
8
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
package/dist/eventcatalog.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from "./chunk-DCLTVJDP.js";
|
|
7
7
|
import {
|
|
8
8
|
log_build_default
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-MF3FQSRF.js";
|
|
10
|
+
import "./chunk-NUHIER2H.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-
|
|
18
|
+
} from "./chunk-C4NENBPQ.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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: {
|
|
@@ -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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
const
|
|
28
|
+
const errorHandlingMiddleware: MiddlewareHandler = async (context, next) => {
|
|
29
|
+
const response = await next();
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
return next();
|
|
39
|
-
}
|
|
40
|
+
return response;
|
|
41
|
+
};
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
9
|
+
"version": "2.44.0",
|
|
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",
|