@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 "&amp;";
124
+ case "<":
125
+ return "&lt;";
126
+ case ">":
127
+ return "&gt;";
128
+ case '"':
129
+ return "&quot;";
130
+ case "'":
131
+ return "&#39;";
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
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Renders the login page markup with optional error messaging.
3
+ */
4
+ export declare function buildLoginPage(options: {
5
+ loginPath: string;
6
+ errorMessage?: string;
7
+ }): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grasp-labs/ds-microfrontends-integration",
3
- "version": "0.6.2",
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",