create-astram-app 0.11.0 → 0.12.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/index.js +98 -4
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
- package/template/frontend/_env.example +7 -0
- package/template/frontend/package.json +2 -2
- package/template/frontend/src/App.jsx +24 -0
- package/template/frontend/src/auth/AuthContext.jsx +73 -30
- package/template/frontend/src/auth/CaptchaWidget.jsx +108 -0
- package/template/frontend/src/auth/captchaProvider.js +23 -0
- package/template/frontend/src/auth/captchaProvider.test.mjs +44 -0
- package/template/frontend/src/auth/captchaRender.js +125 -0
- package/template/frontend/src/auth/captchaRender.test.mjs +182 -0
- package/template/frontend/src/auth/passwordVisibility.js +9 -0
- package/template/frontend/src/auth/passwordVisibility.test.mjs +12 -0
- package/template/frontend/src/auth/reenrollWording.js +29 -0
- package/template/frontend/src/auth/reenrollWording.test.mjs +23 -0
- package/template/frontend/src/auth/routeGate.js +10 -0
- package/template/frontend/src/auth/routeGate.test.mjs +22 -1
- package/template/frontend/src/auth/sessionFlow.js +80 -4
- package/template/frontend/src/auth/sessionFlow.test.mjs +212 -1
- package/template/frontend/src/auth/signInOptions.js +64 -0
- package/template/frontend/src/auth/signInOptions.test.mjs +94 -0
- package/template/frontend/src/auth/useSignInOptions.js +34 -0
- package/template/frontend/src/pages/ForgotPassword.jsx +49 -3
- package/template/frontend/src/pages/Login.jsx +54 -4
- package/template/frontend/src/pages/ResetPassword.jsx +83 -11
- package/template/frontend/src/pages/account/ReenrollPage.jsx +17 -11
- package/template/frontend/src/pages/account/TwoFactorPage.jsx +9 -6
- package/template/frontend/src/pages/audit/AuditPage.jsx +17 -3
- package/template/frontend/src/pages/audit/actorKind.js +17 -0
- package/template/frontend/src/pages/audit/actorKind.test.mjs +23 -0
- package/template/frontend/src/pages/companies/CompaniesPage.jsx +104 -4
- package/template/frontend/src/pages/companies/moveAction.js +61 -0
- package/template/frontend/src/pages/companies/moveAction.test.mjs +91 -0
- package/template/frontend/src/pages/org-structure/OrgStructurePage.jsx +408 -0
- package/template/frontend/src/pages/org-structure/orgStructure.js +140 -0
- package/template/frontend/src/pages/org-structure/orgStructure.test.mjs +199 -0
- package/template/frontend/src/pages/roles/RoleForm.jsx +32 -14
- package/template/frontend/src/pages/roles/RolesPage.jsx +167 -42
- package/template/frontend/src/pages/roles/roleRules.js +104 -0
- package/template/frontend/src/pages/roles/roleRules.test.mjs +178 -0
- package/template/frontend/src/pages/security/PolicyFormDialog.jsx +243 -0
- package/template/frontend/src/pages/security/SecuritySettingsPage.jsx +229 -0
- package/template/frontend/src/pages/security/securityPolicies.js +246 -0
- package/template/frontend/src/pages/security/securityPolicies.test.mjs +429 -0
- package/template/frontend/src/pages/users/MembershipCard.jsx +5 -2
- package/template/frontend/src/pages/users/PostingRow.jsx +13 -10
- package/template/frontend/src/pages/users/SignInActionDialog.jsx +98 -0
- package/template/frontend/src/pages/users/UserForm.jsx +65 -3
- package/template/frontend/src/pages/users/UsersPage.jsx +91 -3
- package/template/frontend/src/pages/users/loginName.js +108 -0
- package/template/frontend/src/pages/users/loginName.test.mjs +133 -0
- package/template/frontend/src/pages/users/signInAdmin.js +122 -0
- package/template/frontend/src/pages/users/signInAdmin.test.mjs +183 -0
- package/template/frontend/src/platform/httpClient.js +40 -28
- package/template/frontend/src/platform/httpClient.test.mjs +71 -0
- package/template/frontend/src/platform/shieldTransport.js +12 -12
- package/template/frontend/src/shell/AppShell.jsx +4 -0
- package/template/frontend/src/shell/GraceBanner.jsx +28 -0
- package/template/frontend/src/shell/graceDeadline.js +50 -0
- package/template/frontend/src/shell/graceDeadline.test.mjs +59 -0
- package/template/frontend/src/shell/nav.js +30 -1
- package/template/frontend/src/viteConfig.test.mjs +63 -0
- package/template/frontend/vite/captchaCsp.js +99 -0
- package/template/frontend/vite.config.js +14 -3
- package/template/server/_env.example +25 -0
- package/template/server/package.json +9 -6
- package/template/server/src/app.module.ts +17 -6
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// viteConfig.test.mjs — the Vite config's own plugins must not return anything from
|
|
2
|
+
// `configureServer` / `configurePreviewServer`. Vite treats a returned function as a POST hook and
|
|
3
|
+
// calls it later with no arguments, so an arrow body like `(s) => s.middlewares.use(fn)` (which
|
|
4
|
+
// returns the connect app) crashed `npm run dev` with "Cannot read properties of undefined
|
|
5
|
+
// (reading 'url')". Middleware is installed during the call; the return value must be undefined.
|
|
6
|
+
//
|
|
7
|
+
// node --test src/viteConfig.test.mjs
|
|
8
|
+
import { test, describe } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import config from '../vite.config.js';
|
|
11
|
+
|
|
12
|
+
/** A fake dev/preview server whose `middlewares.use` returns the app, as connect does. */
|
|
13
|
+
function fakeServer() {
|
|
14
|
+
const used = [];
|
|
15
|
+
const middlewares = {
|
|
16
|
+
use(fn) {
|
|
17
|
+
used.push(fn);
|
|
18
|
+
return middlewares;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
return { server: { middlewares, httpServer: null, config: {} }, used };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ours = (config.plugins ?? [])
|
|
25
|
+
.flat(Infinity)
|
|
26
|
+
.filter((p) => p && typeof p.name === 'string' && p.name.startsWith('astram-'));
|
|
27
|
+
|
|
28
|
+
describe('vite.config.js plugins', () => {
|
|
29
|
+
test('the astram plugins are present', () => {
|
|
30
|
+
assert.ok(ours.some((p) => p.name === 'astram-confirm-email-no-referrer'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const hook of ['configureServer', 'configurePreviewServer']) {
|
|
34
|
+
test(`every astram plugin's ${hook} installs its middleware and returns undefined`, async () => {
|
|
35
|
+
for (const plugin of ours) {
|
|
36
|
+
const fn = plugin[hook];
|
|
37
|
+
if (fn === undefined) continue;
|
|
38
|
+
const { server, used } = fakeServer();
|
|
39
|
+
const handler = typeof fn === 'function' ? fn : fn.handler;
|
|
40
|
+
const out = await handler.call(plugin, server);
|
|
41
|
+
assert.equal(out, undefined, `${plugin.name}.${hook} must not return a post hook`);
|
|
42
|
+
assert.ok(used.length > 0, `${plugin.name}.${hook} installed its middleware`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('the confirm-email middleware sets no-referrer only on /confirm-email', () => {
|
|
48
|
+
const plugin = ours.find((p) => p.name === 'astram-confirm-email-no-referrer');
|
|
49
|
+
const { server, used } = fakeServer();
|
|
50
|
+
plugin.configureServer(server);
|
|
51
|
+
const run = (url) => {
|
|
52
|
+
const headers = {};
|
|
53
|
+
let nexted = false;
|
|
54
|
+
used[0]({ url }, { setHeader: (k, v) => (headers[k] = v) }, () => (nexted = true));
|
|
55
|
+
return { headers, nexted };
|
|
56
|
+
};
|
|
57
|
+
assert.deepEqual(run('/confirm-email?x=1'), {
|
|
58
|
+
headers: { 'Referrer-Policy': 'no-referrer' },
|
|
59
|
+
nexted: true,
|
|
60
|
+
});
|
|
61
|
+
assert.deepEqual(run('/dashboard'), { headers: {}, nexted: true });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAPTCHA origins for the static Content-Security-Policy `<meta>` in index.html (R7.6, D-10).
|
|
3
|
+
*
|
|
4
|
+
* The CSP is fixed when the frontend is built, so the build takes `VITE_CAPTCHA_PROVIDER` (the
|
|
5
|
+
* same value as the server's `CAPTCHA_PROVIDER`, or unset) and adds that ONE provider's fixed
|
|
6
|
+
* origins to `script-src`, `frame-src` and `connect-src`. Never `'unsafe-inline'`. The site key
|
|
7
|
+
* is not baked in: the login page reads it from `GET /auth/login-options` at run time, and shows
|
|
8
|
+
* a "misconfigured" notice if the server's provider differs from this build's.
|
|
9
|
+
*
|
|
10
|
+
* unset / '' index.html is left byte-identical
|
|
11
|
+
* unknown value the build (and dev server) fails with a message
|
|
12
|
+
*
|
|
13
|
+
* Origins, from the providers' CSP guidance:
|
|
14
|
+
* Turnstile https://developers.cloudflare.com/turnstile/reference/content-security-policy/
|
|
15
|
+
* (script-src, frame-src; the widget also connects to challenges.cloudflare.com)
|
|
16
|
+
* reCAPTCHA https://developers.google.com/recaptcha/docs/faq (CSP answer)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const TURNSTILE = 'https://challenges.cloudflare.com';
|
|
20
|
+
const RECAPTCHA = {
|
|
21
|
+
'script-src': ['https://www.google.com/recaptcha/', 'https://www.gstatic.com/recaptcha/'],
|
|
22
|
+
'frame-src': ['https://www.google.com/recaptcha/', 'https://recaptcha.google.com/recaptcha/'],
|
|
23
|
+
'connect-src': ['https://www.google.com/recaptcha/'],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Provider → directive → origins to add. Values match the server's `CAPTCHA_PROVIDER`. */
|
|
27
|
+
export const CAPTCHA_CSP_ORIGINS = Object.freeze({
|
|
28
|
+
turnstile: {
|
|
29
|
+
'script-src': [TURNSTILE],
|
|
30
|
+
'frame-src': [TURNSTILE],
|
|
31
|
+
'connect-src': [TURNSTILE],
|
|
32
|
+
},
|
|
33
|
+
'recaptcha-v3': RECAPTCHA,
|
|
34
|
+
'recaptcha-v2': RECAPTCHA,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const CAPTCHA_PROVIDERS = Object.freeze(Object.keys(CAPTCHA_CSP_ORIGINS));
|
|
38
|
+
|
|
39
|
+
const CSP_META = /(<meta\s+http-equiv="Content-Security-Policy"\s+content=")([^"]*)(")/;
|
|
40
|
+
|
|
41
|
+
/** Throws on a value that is not a known provider; returns null for unset/empty. */
|
|
42
|
+
function checkProvider(provider) {
|
|
43
|
+
if (provider === undefined || provider === null || provider === '') return null;
|
|
44
|
+
if (!Object.hasOwn(CAPTCHA_CSP_ORIGINS, provider)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`VITE_CAPTCHA_PROVIDER="${provider}" is not supported; use one of ` +
|
|
47
|
+
`${CAPTCHA_PROVIDERS.join(', ')}, or leave it unset for no CAPTCHA.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return provider;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Add `provider`'s origins to the CSP `<meta>` in `html`. Unset provider ⇒ `html` unchanged.
|
|
55
|
+
* A directive the policy lacks (`frame-src` today) is added as `'self'` plus the origins, which
|
|
56
|
+
* keeps its old fallback (`default-src 'self'`) for everything else.
|
|
57
|
+
*/
|
|
58
|
+
export function extendCsp(html, provider) {
|
|
59
|
+
const known = checkProvider(provider);
|
|
60
|
+
if (known === null) return html;
|
|
61
|
+
const match = CSP_META.exec(html);
|
|
62
|
+
if (!match) return html;
|
|
63
|
+
|
|
64
|
+
const additions = CAPTCHA_CSP_ORIGINS[known];
|
|
65
|
+
const directives = match[2]
|
|
66
|
+
.split(';')
|
|
67
|
+
.map((d) => d.trim())
|
|
68
|
+
.filter(Boolean);
|
|
69
|
+
for (const [name, origins] of Object.entries(additions)) {
|
|
70
|
+
const index = directives.findIndex((d) => d.split(/\s+/)[0] === name);
|
|
71
|
+
if (index === -1) {
|
|
72
|
+
const after = directives.findIndex((d) => d.split(/\s+/)[0] === 'connect-src');
|
|
73
|
+
const entry = [name, "'self'", ...origins].join(' ');
|
|
74
|
+
directives.splice(after === -1 ? directives.length : after + 1, 0, entry);
|
|
75
|
+
} else {
|
|
76
|
+
const existing = directives[index].split(/\s+/);
|
|
77
|
+
const missing = origins.filter((o) => !existing.includes(o));
|
|
78
|
+
directives[index] = [...existing, ...missing].join(' ');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return html.replace(CSP_META, `$1${directives.join('; ')}$3`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Vite plugin. Reads `VITE_CAPTCHA_PROVIDER` from the resolved config env (`.env*` files and the
|
|
86
|
+
* process environment), so the value is checked once at startup and an unknown one fails the build.
|
|
87
|
+
*/
|
|
88
|
+
export function captchaCsp() {
|
|
89
|
+
let provider = null;
|
|
90
|
+
return {
|
|
91
|
+
name: 'astram-captcha-csp',
|
|
92
|
+
configResolved(config) {
|
|
93
|
+
provider = checkProvider(config.env?.VITE_CAPTCHA_PROVIDER);
|
|
94
|
+
},
|
|
95
|
+
transformIndexHtml(html) {
|
|
96
|
+
return extendCsp(html, provider);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -2,6 +2,7 @@ import { defineConfig } from 'vite';
|
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite';
|
|
4
4
|
import { fileURLToPath, URL } from 'node:url';
|
|
5
|
+
import { captchaCsp } from './vite/captchaCsp.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Dev-server only: relax the Content-Security-Policy in index.html just enough for Vite.
|
|
@@ -34,22 +35,32 @@ function confirmEmailNoReferrer() {
|
|
|
34
35
|
}
|
|
35
36
|
next();
|
|
36
37
|
};
|
|
38
|
+
// Block bodies on purpose: Vite treats a function RETURNED from these hooks as a post hook and
|
|
39
|
+
// calls it later with no arguments, and `middlewares.use()` returns the connect app (a function).
|
|
37
40
|
return {
|
|
38
41
|
name: 'astram-confirm-email-no-referrer',
|
|
39
|
-
configureServer
|
|
40
|
-
|
|
42
|
+
configureServer(server) {
|
|
43
|
+
server.middlewares.use(setHeader);
|
|
44
|
+
},
|
|
45
|
+
configurePreviewServer(server) {
|
|
46
|
+
server.middlewares.use(setHeader);
|
|
47
|
+
},
|
|
41
48
|
};
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/**
|
|
45
52
|
* __APP_NAME__ frontend — Vite config (shadcn/ui + Tailwind v4).
|
|
46
53
|
*
|
|
54
|
+
* `captchaCsp()` adds the CAPTCHA provider's origins to the CSP <meta> when the build sets
|
|
55
|
+
* `VITE_CAPTCHA_PROVIDER` (same value as the server's `CAPTCHA_PROVIDER`; unset ⇒ index.html
|
|
56
|
+
* unchanged, unknown ⇒ the build fails). See vite/captchaCsp.js.
|
|
57
|
+
*
|
|
47
58
|
* `/api` is proxied to the framework server (default :3000), stripping the `/api`
|
|
48
59
|
* prefix so the browser calls same-origin `/api/auth/login` while the server sees
|
|
49
60
|
* `/auth/login`. `@` aliases `src/` (the shadcn import convention).
|
|
50
61
|
*/
|
|
51
62
|
export default defineConfig({
|
|
52
|
-
plugins: [react(), tailwindcss(), devCsp(), confirmEmailNoReferrer()],
|
|
63
|
+
plugins: [react(), tailwindcss(), captchaCsp(), devCsp(), confirmEmailNoReferrer()],
|
|
53
64
|
resolve: {
|
|
54
65
|
alias: {
|
|
55
66
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
@@ -172,6 +172,31 @@ NODE_ENV=development
|
|
|
172
172
|
# INVITE_LINK_TTL_HOURS=72
|
|
173
173
|
# INVITE_RATE_PER_ADMIN=30
|
|
174
174
|
|
|
175
|
+
# --- OPTIONAL: CAPTCHA on sign-in (Cloudflare Turnstile or Google reCAPTCHA) ---
|
|
176
|
+
# Read only when src/app.module.ts passes ShieldModule.forRoot({ captcha: 'env' }); a missing or
|
|
177
|
+
# invalid value then fails boot. One of turnstile | recaptcha-v3 | recaptcha-v2.
|
|
178
|
+
# The frontend build needs the SAME value in frontend/.env as VITE_CAPTCHA_PROVIDER (it adds the
|
|
179
|
+
# provider's origins to the static CSP); `create-astram-app --captcha <provider>` writes both.
|
|
180
|
+
# If the two differ, the login page shows a "misconfigured" notice and loads no widget.
|
|
181
|
+
# CAPTCHA_PROVIDER=turnstile
|
|
182
|
+
# Public site key from the provider's dashboard (served to the login page at run time).
|
|
183
|
+
# CAPTCHA_SITE_KEY=
|
|
184
|
+
# Secret key: outside development/test it MUST be a `secret-ref:` reference (a literal fails boot).
|
|
185
|
+
# CAPTCHA_SECRET=secret-ref:captcha/secret
|
|
186
|
+
# Hostnames a solved challenge must come from, comma-separated (required outside development/test).
|
|
187
|
+
# CAPTCHA_EXPECTED_HOSTNAMES=app.example.com
|
|
188
|
+
# adaptive (challenge after CAPTCHA_THRESHOLD failed attempts) | always.
|
|
189
|
+
# CAPTCHA_MODE=adaptive
|
|
190
|
+
# CAPTCHA_THRESHOLD=3
|
|
191
|
+
# reCAPTCHA v3 only: lowest accepted score (0.0-1.0).
|
|
192
|
+
# CAPTCHA_MIN_SCORE=0.5
|
|
193
|
+
# Provider outage: closed (refuse; default) | open (allow, audited; limits and lockout still apply).
|
|
194
|
+
# CAPTCHA_FAIL_MODE=closed
|
|
195
|
+
# Verification call timeout in ms (default 5000, max 10000).
|
|
196
|
+
# CAPTCHA_TIMEOUT_MS=5000
|
|
197
|
+
# Endpoints that ask for a challenge, comma-separated (default: all three).
|
|
198
|
+
# CAPTCHA_ENDPOINTS=login,forgot_password,reset_password
|
|
199
|
+
|
|
175
200
|
# --- OPTIONAL: organisation bootstrap (`npm run bootstrap:org`; needs IdentityFeatureModule wired) ---
|
|
176
201
|
# Platform-root user uuids, comma-separated, lower-case canonical form. Read by rootIdsFromEnv()
|
|
177
202
|
# in src/app.module.ts (pass it to BOTH forRoot calls); bootstrap:org prints the uuid of each root.
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"postinstall": "node scripts/sync-astram-guide.mjs"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@astram/core": "1.
|
|
25
|
-
"@astram/engine": "0.
|
|
26
|
-
"@astram/guard": "0.
|
|
24
|
+
"@astram/core": "1.11.0",
|
|
25
|
+
"@astram/engine": "0.11.0",
|
|
26
|
+
"@astram/guard": "0.20.0",
|
|
27
27
|
"@astram/guide": "^0.2.8",
|
|
28
|
-
"@astram/identity": "0.
|
|
29
|
-
"@astram/messaging": "^0.1.
|
|
30
|
-
"@astram/migrate": "0.
|
|
28
|
+
"@astram/identity": "0.17.0",
|
|
29
|
+
"@astram/messaging": "^0.1.3",
|
|
30
|
+
"@astram/migrate": "0.7.1",
|
|
31
31
|
"@astram/sdk": "^0.2.0",
|
|
32
32
|
"@nestjs/common": "^11.2.7",
|
|
33
33
|
"@nestjs/core": "^11.2.7",
|
|
@@ -48,5 +48,8 @@
|
|
|
48
48
|
"typescript-eslint": "^8.0.0",
|
|
49
49
|
"unplugin-swc": "^1.5.1",
|
|
50
50
|
"vitest": "^2.1.8"
|
|
51
|
+
},
|
|
52
|
+
"overrides": {
|
|
53
|
+
"vitest": "$vitest"
|
|
51
54
|
}
|
|
52
55
|
}
|
|
@@ -32,7 +32,10 @@ import { DemoController } from './health/demo.controller';
|
|
|
32
32
|
* The framework ships a complete Auth surface you can turn on WITHOUT writing any
|
|
33
33
|
* auth code. It is OFF by default so this app boots with NO `.env` at all (pure
|
|
34
34
|
* enforcement). To enable it:
|
|
35
|
-
* 1. Change `ShieldModule.forRoot()` below to
|
|
35
|
+
* 1. Change `ShieldModule.forRoot()` below to
|
|
36
|
+
* `ShieldModule.forRoot({ auth: true, logoutRequiresProof: true })`. This app's frontend
|
|
37
|
+
* sends its refresh and access tokens on logout, so the server can require that proof
|
|
38
|
+
* (an unproven `POST /auth/logout` then ends nothing).
|
|
36
39
|
* 2. Provide the DB/Redis/HMAC config in `.env` (see the AUTH block in
|
|
37
40
|
* `.env.example`) — auth needs a real MariaDB + Redis and a refresh HMAC key.
|
|
38
41
|
* 3. Create a user in the `shield_identity.identity_credential` table.
|
|
@@ -47,6 +50,14 @@ import { DemoController } from './health/demo.controller';
|
|
|
47
50
|
* Admins can then invite users (`POST /identity/users/:uuid/invite`, the Users page
|
|
48
51
|
* Invite button); `IdentityFeatureModule.forRoot({ invite: { returnLinkToAdmin: true } })`
|
|
49
52
|
* also shows the one-time link to the admin once.
|
|
53
|
+
* 5. Optional sign-in options (all off by default; see CAPABILITIES.md):
|
|
54
|
+
* ShieldModule.forRoot({ auth: true, logoutRequiresProof: true,
|
|
55
|
+
* signInWith: ['email', 'username'], // one field takes the email or the login name
|
|
56
|
+
* captcha: 'env' }) // CAPTCHA_* in `.env.example`, and the same
|
|
57
|
+
* // provider as VITE_CAPTCHA_PROVIDER in frontend
|
|
58
|
+
* Login names are set by an admin (Users page) or by `bootstrap:org`, never by the user.
|
|
59
|
+
* Mandatory 2FA is not an option here: with IdentityFeatureModule wired, admins set it
|
|
60
|
+
* per tenant, role or permission on the Security page (`/identity/security-policies`).
|
|
50
61
|
* You then get framework-owned `POST /auth/{login,mfa,refresh,logout}` for free —
|
|
51
62
|
* no controllers to write. (Leaving `auth` off keeps the zero-config enforcement
|
|
52
63
|
* boot; turning it on requires the config above, by design — fail-closed.)
|
|
@@ -63,7 +74,7 @@ import { DemoController } from './health/demo.controller';
|
|
|
63
74
|
* import { identityAuthActionKeys, identityControlPlaneKeys,
|
|
64
75
|
* IdentityFeatureModule } from '@astram/identity';
|
|
65
76
|
* imports: [
|
|
66
|
-
* ShieldModule.forRoot({ auth: true, rootIds: rootIdsFromEnv(),
|
|
77
|
+
* ShieldModule.forRoot({ auth: true, logoutRequiresProof: true, rootIds: rootIdsFromEnv(),
|
|
67
78
|
* identityActionKeys: identityAuthActionKeys, identityControlPlaneKeys }),
|
|
68
79
|
* // mounts /identity/{companies,users,roles,…}
|
|
69
80
|
* IdentityFeatureModule.forRoot({ rootIds: rootIdsFromEnv() }),
|
|
@@ -89,8 +100,8 @@ import { DemoController } from './health/demo.controller';
|
|
|
89
100
|
* const extraObjects = [Note];
|
|
90
101
|
* const { actionKeys, controlPlaneKeys } = buildIdentityAuthKeys(extraObjects);
|
|
91
102
|
* imports: [
|
|
92
|
-
* ShieldModule.forRoot({ auth: true,
|
|
93
|
-
* identityControlPlaneKeys: controlPlaneKeys }),
|
|
103
|
+
* ShieldModule.forRoot({ auth: true, logoutRequiresProof: true,
|
|
104
|
+
* identityActionKeys: actionKeys, identityControlPlaneKeys: controlPlaneKeys }),
|
|
94
105
|
* IdentityFeatureModule.forRoot({ extraObjects }),
|
|
95
106
|
* ],
|
|
96
107
|
* The frontend ships the matching pages/widget (RecordPoliciesPage, ShareDialog)
|
|
@@ -101,8 +112,8 @@ import { DemoController } from './health/demo.controller';
|
|
|
101
112
|
// admin console (incl. the guided rule-builder + owner shares), use (see the OPTIONAL
|
|
102
113
|
// blocks above for the full form + the extraObjects note for your OWN owned types):
|
|
103
114
|
// imports: [
|
|
104
|
-
// ShieldModule.forRoot({ auth: true, rootIds: rootIdsFromEnv(),
|
|
105
|
-
// identityControlPlaneKeys }),
|
|
115
|
+
// ShieldModule.forRoot({ auth: true, logoutRequiresProof: true, rootIds: rootIdsFromEnv(),
|
|
116
|
+
// identityActionKeys, identityControlPlaneKeys }),
|
|
106
117
|
// IdentityFeatureModule.forRoot({ rootIds: rootIdsFromEnv() /*, extraObjects: [YourType] */ }),
|
|
107
118
|
// ],
|
|
108
119
|
imports: [ShieldModule.forRoot()],
|