@grasp-labs/ds-microfrontends-integration 0.6.2 → 0.7.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/README.md
CHANGED
|
@@ -218,6 +218,43 @@ These dependencies are configured as singletons to prevent multiple instances an
|
|
|
218
218
|
npm install
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
+
### Dev auth middleware
|
|
222
|
+
|
|
223
|
+
For local Express work you can protect every route with the provided development
|
|
224
|
+
login middleware:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import express from "express";
|
|
228
|
+
import { devLoginMiddleware } from "@grasp-labs/ds-microfrontends-integration/dev";
|
|
229
|
+
|
|
230
|
+
const app = express();
|
|
231
|
+
app.use(devLoginMiddleware());
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
To skip the login form entirely, preload `DEV_AUTH_TOKEN` (for example by adding `DEV_AUTH_TOKEN=<value>` to a `.env` file or exporting it in your shell) before starting the server.
|
|
235
|
+
|
|
236
|
+
Once you have a token you can forward it to internal services through your dev proxy. For example:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { DEV_AUTH_TOKEN } from "@grasp-labs/ds-microfrontends-integration/dev";
|
|
240
|
+
import { createProxyMiddleware } from "http-proxy-middleware";
|
|
241
|
+
|
|
242
|
+
expressInstance.use(
|
|
243
|
+
`${BASE_PATH}/api/ds-config-api`,
|
|
244
|
+
createProxyMiddleware({
|
|
245
|
+
target: "https://your-backend-url.com",
|
|
246
|
+
on: {
|
|
247
|
+
proxyReq: (proxyReq) => {
|
|
248
|
+
const token = DEV_AUTH_TOKEN.get();
|
|
249
|
+
if (token) {
|
|
250
|
+
proxyReq.setHeader("Authorization", `Bearer ${token}`);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
```
|
|
257
|
+
|
|
221
258
|
### Scripts
|
|
222
259
|
|
|
223
260
|
- `npm run build` - Build the library
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
export type DevLoginOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Path segment or absolute path for the login page. Values are trimmed and
|
|
5
|
+
* automatically prefixed with "/" when missing (e.g. "__login" => "/__login").
|
|
6
|
+
* @default "/__login"
|
|
7
|
+
*/
|
|
8
|
+
loginPath?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Location to redirect to after successful authentication. The same
|
|
11
|
+
* normalization rules apply as for {@link loginPath}.
|
|
12
|
+
* @default "/"
|
|
13
|
+
*/
|
|
14
|
+
redirectAfterLoginPathTo?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Fully qualified URL of the backend dev auth login endpoint.
|
|
17
|
+
* @default "https://auth-dev.grasp-daas.com/rest-auth/login/"
|
|
18
|
+
*/
|
|
19
|
+
authEndpoint?: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Creates an Express middleware that guards all routes behind a simple dev login.
|
|
23
|
+
* Users are redirected to the login page unless a `DEV_AUTH_TOKEN` is present.
|
|
24
|
+
* Configuration paths are normalized so callers can pass either "foo" or "/foo".
|
|
25
|
+
* The middleware internally parses form submissions via `express.urlencoded({ extended: true })`.
|
|
26
|
+
*
|
|
27
|
+
* To skip the HTML login entirely during local development, preload
|
|
28
|
+
* `DEV_AUTH_TOKEN` (for example via a `.env` file or by exporting it before
|
|
29
|
+
* starting Express). You can obtain a valid token by calling the dev auth login
|
|
30
|
+
* endpoint (defaults to `https://auth-dev.grasp-daas.com/rest-auth/login/`,
|
|
31
|
+
* overridable via {@link DevLoginOptions.authEndpoint}) with the same
|
|
32
|
+
* credentials you would submit through the form and copying the returned
|
|
33
|
+
* `access_token`.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import express from 'express';
|
|
38
|
+
* import { devLoginMiddleware } from '@grasp-labs/ds-microfrontends-integration/dev';
|
|
39
|
+
*
|
|
40
|
+
* const app = express();
|
|
41
|
+
* app.use(devLoginMiddleware());
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function devLoginMiddleware(options?: DevLoginOptions): (req: Request, res: Response, next: NextFunction) => Promise<any>;
|
|
45
|
+
/**
|
|
46
|
+
* Simple in-memory token store backed by process.env.
|
|
47
|
+
* Preloading `DEV_AUTH_TOKEN` (e.g. via a `.env` file or shell env var) skips the login flow entirely.
|
|
48
|
+
*/
|
|
49
|
+
export declare const DEV_AUTH_TOKEN: {
|
|
50
|
+
readonly get: () => string | undefined;
|
|
51
|
+
readonly set: (token: string) => void;
|
|
52
|
+
readonly exists: () => boolean;
|
|
53
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { urlencoded as b } from "express";
|
|
2
|
+
function w(e) {
|
|
3
|
+
const t = e.loginPath, r = e.errorMessage ?? "", n = y(r);
|
|
4
|
+
return `<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<title>Login</title>
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--border: 1px solid oklch(100% 0 0/0.1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
* {
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 0;
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
font-size: 14px;
|
|
20
|
+
border-radius: 8px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
body {
|
|
24
|
+
font-family: sans-serif;
|
|
25
|
+
background-color: oklch(0.14 0 0);
|
|
26
|
+
color: oklch(1 0 0);
|
|
27
|
+
display: flex;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
padding-top: 20svh;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main {
|
|
33
|
+
width: 100%;
|
|
34
|
+
max-width: 320px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
h1 {
|
|
38
|
+
font-size: 24px;
|
|
39
|
+
margin-bottom: 8px;
|
|
40
|
+
color: oklch(0.95 0 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
p {
|
|
44
|
+
color: oklch(0.6 0 0);
|
|
45
|
+
margin-bottom: 24px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
form {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: 14px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
input {
|
|
55
|
+
padding: 10px 12px;
|
|
56
|
+
border: var(--border);
|
|
57
|
+
background-color: oklch(0.2 0 0);
|
|
58
|
+
color: oklch(0.95 0 0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
input:focus {
|
|
62
|
+
outline: none;
|
|
63
|
+
border-color: oklch(0.7 0 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
button {
|
|
67
|
+
padding: 10px 12px;
|
|
68
|
+
border: none;
|
|
69
|
+
background-color: oklch(0.488 0.243 264.376);
|
|
70
|
+
color: white;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
button:hover {
|
|
75
|
+
background-color: oklch(0.588 0.243 264.376);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.error-block {
|
|
79
|
+
padding: 12px;
|
|
80
|
+
margin-top: 20px;
|
|
81
|
+
border: var(--border);
|
|
82
|
+
background-color: oklch(0.20 0 0);
|
|
83
|
+
color: oklch(0.71 0.17 22);
|
|
84
|
+
margin-bottom: 14px;
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.error-block pre {
|
|
89
|
+
margin: 0;
|
|
90
|
+
white-space: pre-wrap;
|
|
91
|
+
word-break: break-word;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<main>
|
|
97
|
+
<div>
|
|
98
|
+
<h1>Login</h1>
|
|
99
|
+
<p>Provide credentials to unlock the dev server</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<form method="POST" action="${t}">
|
|
103
|
+
<input type="text" placeholder="Email" name="email" required />
|
|
104
|
+
<input
|
|
105
|
+
type="password"
|
|
106
|
+
placeholder="Password"
|
|
107
|
+
name="password"
|
|
108
|
+
required
|
|
109
|
+
/>
|
|
110
|
+
<button type="submit">Login</button>
|
|
111
|
+
<div class="error-block" style="display: ${r ? "block" : "none"};">
|
|
112
|
+
<pre>${n}</pre>
|
|
113
|
+
</div>
|
|
114
|
+
</form>
|
|
115
|
+
</main>
|
|
116
|
+
</body>
|
|
117
|
+
</html>`;
|
|
118
|
+
}
|
|
119
|
+
function y(e) {
|
|
120
|
+
return e.replace(/[&<>"']/g, (t) => {
|
|
121
|
+
switch (t) {
|
|
122
|
+
case "&":
|
|
123
|
+
return "&";
|
|
124
|
+
case "<":
|
|
125
|
+
return "<";
|
|
126
|
+
case ">":
|
|
127
|
+
return ">";
|
|
128
|
+
case '"':
|
|
129
|
+
return """;
|
|
130
|
+
case "'":
|
|
131
|
+
return "'";
|
|
132
|
+
default:
|
|
133
|
+
return t;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function E(e = {}) {
|
|
138
|
+
const {
|
|
139
|
+
loginPath: t = "/__login",
|
|
140
|
+
redirectAfterLoginPathTo: r = "/",
|
|
141
|
+
authEndpoint: n = "https://auth-dev.grasp-daas.com/rest-auth/login/"
|
|
142
|
+
} = e, o = h(t, "loginPath"), c = h(
|
|
143
|
+
r,
|
|
144
|
+
"redirectAfterLoginPathTo"
|
|
145
|
+
), m = k();
|
|
146
|
+
return async (s, i, l) => {
|
|
147
|
+
if (g.exists())
|
|
148
|
+
return s.path === o ? i.redirect(c) : l();
|
|
149
|
+
if (s.path !== o)
|
|
150
|
+
return i.redirect(o);
|
|
151
|
+
if (s.method === "GET")
|
|
152
|
+
return d(i, o);
|
|
153
|
+
if (s.method === "POST") {
|
|
154
|
+
try {
|
|
155
|
+
await m(s, i);
|
|
156
|
+
} catch (a) {
|
|
157
|
+
return l(a);
|
|
158
|
+
}
|
|
159
|
+
const { email: p, password: u } = s.body ?? {};
|
|
160
|
+
if (!p || !u)
|
|
161
|
+
return d(i, o, "Email and password are required.");
|
|
162
|
+
try {
|
|
163
|
+
const { access_token: a } = await x({
|
|
164
|
+
email: p,
|
|
165
|
+
password: u,
|
|
166
|
+
authEndpoint: n
|
|
167
|
+
});
|
|
168
|
+
return g.set(a), i.redirect(303, c);
|
|
169
|
+
} catch (a) {
|
|
170
|
+
const f = `${a instanceof Error ? a.message : String(a)}`;
|
|
171
|
+
return d(i, o, f);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return i.status(405).send("Method Not Allowed");
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function h(e, t) {
|
|
178
|
+
const r = e.trim();
|
|
179
|
+
if (!r)
|
|
180
|
+
throw new Error(
|
|
181
|
+
`devLoginMiddleware ${t} must be a non-empty string`
|
|
182
|
+
);
|
|
183
|
+
return r.startsWith("/") ? r : `/${r}`;
|
|
184
|
+
}
|
|
185
|
+
function d(e, t, r) {
|
|
186
|
+
return e.type("html").send(
|
|
187
|
+
w({
|
|
188
|
+
loginPath: t,
|
|
189
|
+
errorMessage: r
|
|
190
|
+
})
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
function k() {
|
|
194
|
+
const e = b({ extended: !0 });
|
|
195
|
+
return (t, r) => new Promise((n, o) => {
|
|
196
|
+
e(t, r, (c) => {
|
|
197
|
+
if (c) {
|
|
198
|
+
o(c);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
n();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function x({
|
|
206
|
+
email: e,
|
|
207
|
+
password: t,
|
|
208
|
+
authEndpoint: r
|
|
209
|
+
}) {
|
|
210
|
+
const n = await fetch(r, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": "application/json"
|
|
214
|
+
},
|
|
215
|
+
body: JSON.stringify({
|
|
216
|
+
email: e,
|
|
217
|
+
password: t,
|
|
218
|
+
remember_me: !0
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
if (!n.ok)
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Authentication failed with status ${n.status} and response: ${await n.text()}`
|
|
224
|
+
);
|
|
225
|
+
const o = await n.json();
|
|
226
|
+
if (!("access_token" in o && "refresh_token" in o))
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Authentication response is missing access_token or refresh_token: ${JSON.stringify(o, null, 2)}`
|
|
229
|
+
);
|
|
230
|
+
return o;
|
|
231
|
+
}
|
|
232
|
+
const g = {
|
|
233
|
+
get: () => process.env.DEV_AUTH_TOKEN,
|
|
234
|
+
set: (e) => {
|
|
235
|
+
process.env.DEV_AUTH_TOKEN = e;
|
|
236
|
+
},
|
|
237
|
+
exists: () => typeof process.env.DEV_AUTH_TOKEN == "string" && process.env.DEV_AUTH_TOKEN.length > 0
|
|
238
|
+
};
|
|
239
|
+
export {
|
|
240
|
+
g as DEV_AUTH_TOKEN,
|
|
241
|
+
E as devLoginMiddleware
|
|
242
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grasp-labs/ds-microfrontends-integration",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"import": "./dist/mf-common.js",
|
|
18
18
|
"types": "./dist/mf-common.d.ts"
|
|
19
19
|
},
|
|
20
|
+
"./dev": {
|
|
21
|
+
"import": "./dist/dev/express-auth-middleware.js",
|
|
22
|
+
"types": "./dist/dev/express-auth-middleware.d.ts"
|
|
23
|
+
},
|
|
20
24
|
"./styles.css": "./src/index.css"
|
|
21
25
|
},
|
|
22
26
|
"files": [
|
|
@@ -44,11 +48,17 @@
|
|
|
44
48
|
"@module-federation/vite": "^1.0.0",
|
|
45
49
|
"ajv": "^8.0.0",
|
|
46
50
|
"axios": "^1.7.9",
|
|
51
|
+
"express": "^5.0.0",
|
|
47
52
|
"react": "^19.1.0",
|
|
48
53
|
"react-dom": "^19.1.0",
|
|
49
54
|
"react-hook-form": "^7.0.0",
|
|
50
55
|
"react-router": "^7.8.2"
|
|
51
56
|
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"express": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
52
62
|
"devDependencies": {
|
|
53
63
|
"@eslint/js": "^9.32.0",
|
|
54
64
|
"@grasp-labs/ds-react-components": "^0.13.0",
|
|
@@ -60,6 +70,7 @@
|
|
|
60
70
|
"@testing-library/jest-dom": "^6.6.3",
|
|
61
71
|
"@testing-library/react": "^16.3.0",
|
|
62
72
|
"@testing-library/user-event": "^14.6.1",
|
|
73
|
+
"@types/express": "^5.0.5",
|
|
63
74
|
"@types/node": "^22.16.5",
|
|
64
75
|
"@types/react": "^19.1.10",
|
|
65
76
|
"@types/react-dom": "^19.1.7",
|
|
@@ -71,6 +82,7 @@
|
|
|
71
82
|
"eslint-plugin-prettier": "^5.5.3",
|
|
72
83
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
73
84
|
"eslint-plugin-storybook": "^9.1.0",
|
|
85
|
+
"express": "^5.1.0",
|
|
74
86
|
"globals": "^16.2.0",
|
|
75
87
|
"jsdom": "^26.1.0",
|
|
76
88
|
"json-edit-react": "^1.29.0",
|