@antelopejs/dms-frontend 0.1.2 → 0.1.4
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 +42 -0
- package/package.json +16 -16
- package/templates/vue/server/auth/routes.mjs +67 -2
- package/templates/vue/server.mjs +1 -1
package/README.md
CHANGED
|
@@ -162,6 +162,47 @@ one shorter than 32 characters — the login page at `/auth` fails the first
|
|
|
162
162
|
sign-in attempt rather than starting degraded. Generate one with
|
|
163
163
|
`openssl rand -hex 32`.
|
|
164
164
|
|
|
165
|
+
### Opening a session from a module flow
|
|
166
|
+
|
|
167
|
+
`/auth/login`, `/auth/signup` and `/auth/verify-2fa` are not the only ways a
|
|
168
|
+
visitor becomes authenticated: a module can own a flow that ends in an
|
|
169
|
+
authenticated user — a self-service registration completing after payment, an
|
|
170
|
+
invitation being redeemed — and needs the session cookie opened at the end of
|
|
171
|
+
it. `POST /auth/establish` is the generic form of those three routes.
|
|
172
|
+
|
|
173
|
+
The browser names a backend endpoint and the payload to send it:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
await $fetch("/auth/establish", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: {
|
|
179
|
+
endpoint: "/api/saas/register/finalize",
|
|
180
|
+
payload: { /* whatever that backend route expects */ },
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The frontend server calls that endpoint itself over its own server-to-server
|
|
186
|
+
channel to `DMS_API_BASE_URL`, exactly as it calls `/api/auth/login`, and
|
|
187
|
+
writes the session from the token pair the backend answers with. The browser
|
|
188
|
+
never sends a token and never receives one: it gets back the same
|
|
189
|
+
`{ user, account }` body the login route returns, and the two-factor and
|
|
190
|
+
tenant-assignment outcomes are handled identically.
|
|
191
|
+
|
|
192
|
+
Because the route turns a backend endpoint into a login, it only calls the
|
|
193
|
+
endpoints the deployment names:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# .env
|
|
197
|
+
DMS_AUTH_ESTABLISH_ENDPOINTS=/api/saas/register/finalize,/api/invites/redeem
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The list is empty by default and matched verbatim against absolute `/api/…`
|
|
201
|
+
paths — no prefixes, no query strings, no traversal — so no backend route that
|
|
202
|
+
happens to mint a token pair can be turned into a login by a request from the
|
|
203
|
+
browser. An undeclared endpoint is answered `403` and never called. The route
|
|
204
|
+
is `POST`-only and same-origin, like every other auth action.
|
|
205
|
+
|
|
165
206
|
### Workspaces
|
|
166
207
|
|
|
167
208
|
Each canonical backend URL gets its own owner-only workspace under
|
|
@@ -205,6 +246,7 @@ frontend-module registry drives server and client entries.
|
|
|
205
246
|
| | `DMS_COOKIE_SECURE` | Secure cookies (`true` by default; `ajs dms dev` defaults to `false`) |
|
|
206
247
|
| | `DMS_TRUSTED_PROXY_HOPS` | Number of trusted, rightmost reverse-proxy hops (default `0`) |
|
|
207
248
|
| | `DMS_SESSION_SECRET` | Session cookie encryption key, 32 characters or more (required for login) |
|
|
249
|
+
| | `DMS_AUTH_ESTABLISH_ENDPOINTS` | Backend endpoints `/auth/establish` may open a session from (comma-separated, empty by default) |
|
|
208
250
|
| | `DMS_CLIENT_BASE_URL` | Public frontend URL used in generated links and emails |
|
|
209
251
|
|
|
210
252
|
All of these can be set in the project's `.env` instead of the environment; see [Configuration](#configuration).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms-frontend",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelope",
|
|
@@ -50,36 +50,36 @@
|
|
|
50
50
|
"chalk": "^4.1.2",
|
|
51
51
|
"chokidar": "^4.0.3",
|
|
52
52
|
"commander": "^13.1.0",
|
|
53
|
-
"figlet": "^1.
|
|
53
|
+
"figlet": "^1.9.4",
|
|
54
54
|
"ignore": "^7.0.5",
|
|
55
|
-
"semver": "^7.7.
|
|
55
|
+
"semver": "^7.7.4",
|
|
56
56
|
"unzipper": "^0.12.3"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@antelopejs/tooling-configs": "
|
|
60
|
-
"@inertiajs/vue3": "^3.7.
|
|
59
|
+
"@antelopejs/tooling-configs": ">=0.0.6 <1.0.0",
|
|
60
|
+
"@inertiajs/vue3": "^3.7.1",
|
|
61
61
|
"@types/figlet": "^1.7.0",
|
|
62
|
-
"@types/node": "^24.
|
|
63
|
-
"@types/semver": "^7.7.
|
|
62
|
+
"@types/node": "^24.8.1",
|
|
63
|
+
"@types/semver": "^7.7.1",
|
|
64
64
|
"@types/unzipper": "^0.10.11",
|
|
65
|
-
"@unhead/vue": "^2.
|
|
66
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
67
|
-
"@vue/server-renderer": "^3.5.
|
|
65
|
+
"@unhead/vue": "^2.1.4",
|
|
66
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
67
|
+
"@vue/server-renderer": "^3.5.25",
|
|
68
68
|
"defu": "^6.1.4",
|
|
69
69
|
"knip": "^6.34.0",
|
|
70
|
-
"ofetch": "^1.
|
|
70
|
+
"ofetch": "^1.5.1",
|
|
71
71
|
"oxfmt": "^0.66.0",
|
|
72
72
|
"oxlint": "1.81.0",
|
|
73
73
|
"oxlint-tsgolint": "^7.0.2001",
|
|
74
74
|
"release-it": "^19.2.4",
|
|
75
75
|
"release-it-changelogen": "^0.1.0",
|
|
76
|
-
"rimraf": "^6.
|
|
76
|
+
"rimraf": "^6.1.2",
|
|
77
77
|
"tsx": "^4.20.6",
|
|
78
|
-
"typescript": "^5.
|
|
78
|
+
"typescript": "^5.9.3",
|
|
79
79
|
"unplugin-auto-import": "^20.2.0",
|
|
80
|
-
"vite": "^7.
|
|
81
|
-
"vue": "^3.5.
|
|
82
|
-
"vue-i18n": "^11.
|
|
80
|
+
"vite": "^7.2.4",
|
|
81
|
+
"vue": "^3.5.25",
|
|
82
|
+
"vue-i18n": "^11.2.7"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|
|
85
85
|
"@antelopejs/core": ">=1.6.0 <2"
|
|
@@ -23,6 +23,11 @@ const PASSTHROUGH = new Map([
|
|
|
23
23
|
["/auth/request-2fa-email", "/api/auth/request-2fa-email"],
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
+
// Absolute backend API paths only: no scheme, no authority, no query string,
|
|
27
|
+
// and no segment that could climb out of `/api/`.
|
|
28
|
+
const BACKEND_API_PATH =
|
|
29
|
+
/^\/api\/[A-Za-z0-9][A-Za-z0-9._~-]*(?:\/[A-Za-z0-9][A-Za-z0-9._~-]*)*$/;
|
|
30
|
+
|
|
26
31
|
export function publicSession(session) {
|
|
27
32
|
if (!session) return {};
|
|
28
33
|
return {
|
|
@@ -64,10 +69,10 @@ function singleFlight(token, operation) {
|
|
|
64
69
|
return promise;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
async function
|
|
72
|
+
async function establishWith(request, response, endpoint, payload) {
|
|
68
73
|
const result = await backend(endpoint, request, {
|
|
69
74
|
method: "POST",
|
|
70
|
-
body:
|
|
75
|
+
body: payload,
|
|
71
76
|
});
|
|
72
77
|
if (result.requires_2fa || result.requires_tenant_assignment)
|
|
73
78
|
return json(response, 200, result);
|
|
@@ -76,6 +81,65 @@ async function establish(request, response, endpoint) {
|
|
|
76
81
|
json(response, 200, { user: result.user, account });
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
async function establish(request, response, endpoint) {
|
|
85
|
+
return establishWith(request, response, endpoint, await body(request));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Backend endpoints this deployment lets a module open a session from.
|
|
90
|
+
*
|
|
91
|
+
* Empty by default: a module route only becomes a session-opening route once
|
|
92
|
+
* the operator names it, so no backend endpoint that happens to mint a token
|
|
93
|
+
* pair can be turned into a login by a request from the browser.
|
|
94
|
+
*
|
|
95
|
+
* @param declaration Comma-separated absolute backend paths
|
|
96
|
+
* @returns The declared paths that are well-formed backend API paths
|
|
97
|
+
*/
|
|
98
|
+
export function allowedEstablishEndpoints(
|
|
99
|
+
declaration = process.env.DMS_AUTH_ESTABLISH_ENDPOINTS,
|
|
100
|
+
) {
|
|
101
|
+
return (declaration ?? "")
|
|
102
|
+
.split(",")
|
|
103
|
+
.map((entry) => entry.trim())
|
|
104
|
+
.filter((entry) => BACKEND_API_PATH.test(entry));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Whether a caller-named endpoint is one of the declared ones.
|
|
109
|
+
*
|
|
110
|
+
* The grammar is checked on the caller's value too, so a declaration that was
|
|
111
|
+
* never meant to be a prefix cannot be widened by a traversal or a query
|
|
112
|
+
* string smuggled into the request.
|
|
113
|
+
*
|
|
114
|
+
* @param endpoint Endpoint the browser asked to establish a session from
|
|
115
|
+
* @param allowed Declared endpoints
|
|
116
|
+
* @returns True when the endpoint may be called
|
|
117
|
+
*/
|
|
118
|
+
export function isAllowedEstablishEndpoint(endpoint, allowed) {
|
|
119
|
+
return (
|
|
120
|
+
typeof endpoint === "string" &&
|
|
121
|
+
BACKEND_API_PATH.test(endpoint) &&
|
|
122
|
+
allowed.includes(endpoint)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Open a session from a backend endpoint that mints a token pair.
|
|
128
|
+
*
|
|
129
|
+
* The generic half of `/auth/login`: a module whose own flow ends in an
|
|
130
|
+
* authenticated user — a self-service registration completing, an invitation
|
|
131
|
+
* being redeemed — points this at the backend route that finishes it, and the
|
|
132
|
+
* session cookie is written from the tokens the loader fetched itself. The
|
|
133
|
+
* browser never carries a token: it names an endpoint and a payload, and gets
|
|
134
|
+
* back the same `{ user, account }` the login route answers.
|
|
135
|
+
*/
|
|
136
|
+
async function establishFromEndpoint(request, response) {
|
|
137
|
+
const input = await body(request);
|
|
138
|
+
if (!isAllowedEstablishEndpoint(input.endpoint, allowedEstablishEndpoints()))
|
|
139
|
+
return json(response, 403, { error: "Forbidden" });
|
|
140
|
+
return establishWith(request, response, input.endpoint, input.payload ?? {});
|
|
141
|
+
}
|
|
142
|
+
|
|
79
143
|
export async function refreshSession(request, response) {
|
|
80
144
|
const session = readSession(request);
|
|
81
145
|
if (!session?.refreshToken) return undefined;
|
|
@@ -209,6 +273,7 @@ const actions = {
|
|
|
209
273
|
establish(request, response, "/api/auth/signup"),
|
|
210
274
|
"/auth/verify-2fa": (request, response) =>
|
|
211
275
|
establish(request, response, "/api/auth/verify-2fa"),
|
|
276
|
+
"/auth/establish": establishFromEndpoint,
|
|
212
277
|
"/auth/switch-account": switchAccount,
|
|
213
278
|
"/auth/switch-tenant": switchTenant,
|
|
214
279
|
"/auth/validate-account": validateAccount,
|
package/templates/vue/server.mjs
CHANGED
|
@@ -56,7 +56,7 @@ const AUTH_SERVER_ROUTES = [
|
|
|
56
56
|
["DELETE", /^\/api\/_auth\/session\/?$/],
|
|
57
57
|
[
|
|
58
58
|
"POST",
|
|
59
|
-
/^\/auth\/(?:login|signup|verify-2fa|request-2fa-email|switch-account|switch-tenant|validate-account|remove-account)\/?$/,
|
|
59
|
+
/^\/auth\/(?:login|signup|verify-2fa|establish|request-2fa-email|switch-account|switch-tenant|validate-account|remove-account)\/?$/,
|
|
60
60
|
],
|
|
61
61
|
["POST", /^\/auth\/oauth\/handoff\/?$/],
|
|
62
62
|
["GET", /^\/auth\/oauth\/[^/]+\/(?:start|callback)\/?$/],
|