@grasp-labs/ds-microfrontends-integration 0.6.2 → 0.8.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,21 @@ 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
|
+
`devAuthMiddleware` adds a quick login screen to your local Express setup and reuses the returned token for every backend call. Mount it before anything that proxies to your APIs so outbound requests get `Authorization: Bearer <token>` automatically.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import express from "express";
|
|
227
|
+
import { devAuthMiddleware } from "@grasp-labs/ds-microfrontends-integration/dev";
|
|
228
|
+
|
|
229
|
+
const app = express();
|
|
230
|
+
|
|
231
|
+
app.use(devAuthMiddleware());
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Skip the form entirely by setting `DEV_AUTH_TOKEN` (env var or `.env` file) before starting the server. Need a token? Call the dev auth endpoint with your usual credentials and copy the `access_token`.
|
|
235
|
+
|
|
221
236
|
### Scripts
|
|
222
237
|
|
|
223
238
|
- `npm run build` - Build the library
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
* Simple Express middleware that forces a one-page dev login and then injects
|
|
23
|
+
* `Authorization: Bearer <token>` for subsequent requests. Mount it ahead of
|
|
24
|
+
* any backend proxy (e.g. `createProxyMiddleware`) so outgoing calls receive
|
|
25
|
+
* the token.
|
|
26
|
+
*
|
|
27
|
+
* Skip the login screen by setting `DEV_AUTH_TOKEN` before starting your app
|
|
28
|
+
* (via `.env` or the shell). Tokens can be created by calling the dev auth
|
|
29
|
+
* endpoint with your normal credentials and copying the `access_token`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import express from 'express';
|
|
34
|
+
* import { devAuthMiddleware } from '@grasp-labs/ds-microfrontends-integration/dev';
|
|
35
|
+
*
|
|
36
|
+
* const app = express();
|
|
37
|
+
* app.use(devAuthMiddleware());
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function devAuthMiddleware(options?: DevLoginOptions): (req: Request, res: Response, next: NextFunction) => Promise<any>;
|
|
41
|
+
/**
|
|
42
|
+
* Simple in-memory token store backed by process.env.
|
|
43
|
+
* Preloading `DEV_AUTH_TOKEN` (e.g. via a `.env` file or shell env var) skips the login flow entirely.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DEV_AUTH_TOKEN: {
|
|
46
|
+
readonly get: () => string | undefined;
|
|
47
|
+
readonly set: (token: string) => void;
|
|
48
|
+
readonly exists: () => boolean;
|
|
49
|
+
};
|
|
@@ -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 = g(t, "loginPath"), c = g(
|
|
143
|
+
r,
|
|
144
|
+
"redirectAfterLoginPathTo"
|
|
145
|
+
), m = k();
|
|
146
|
+
return async (s, i, p) => {
|
|
147
|
+
if (l.exists())
|
|
148
|
+
return s.path === o ? i.redirect(c) : (s.headers.authorization = `Bearer ${l.get()}`, p());
|
|
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 p(a);
|
|
158
|
+
}
|
|
159
|
+
const { email: u, password: h } = s.body ?? {};
|
|
160
|
+
if (!u || !h)
|
|
161
|
+
return d(i, o, "Email and password are required.");
|
|
162
|
+
try {
|
|
163
|
+
const { access_token: a } = await x({
|
|
164
|
+
email: u,
|
|
165
|
+
password: h,
|
|
166
|
+
authEndpoint: n
|
|
167
|
+
});
|
|
168
|
+
return l.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 g(e, t) {
|
|
178
|
+
const r = e.trim();
|
|
179
|
+
if (!r)
|
|
180
|
+
throw new Error(
|
|
181
|
+
`devAuthMiddleware ${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 l = {
|
|
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
|
+
l as DEV_AUTH_TOKEN,
|
|
241
|
+
E as devAuthMiddleware
|
|
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.8.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",
|