@adonis-agora/authkit-server 0.90.2 → 1.0.0-rc.1
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/CHANGELOG.md +3654 -0
- package/build/commands/clients_create.d.ts +1 -0
- package/build/commands/clients_create.js +15 -2
- package/build/commands/commands.json +7 -0
- package/build/host/ui/admin.html +1 -1
- package/build/host/views/account/apps.edge +8 -3
- package/build/host/views/partials/styles.edge +1 -1
- package/build/index.d.ts +5 -3
- package/build/index.js +4 -5
- package/build/providers/authkit_server_provider.js +8 -0
- package/build/src/accounts/lucid_account_store.d.ts +0 -18
- package/build/src/accounts/lucid_account_store.js +7 -48
- package/build/src/accounts/lucid_store/mfa.js +1 -1
- package/build/src/accounts/lucid_store/organizations.d.ts +1 -1
- package/build/src/accounts/lucid_store/password_hygiene.d.ts +2 -3
- package/build/src/accounts/lucid_store/password_hygiene.js +2 -3
- package/build/src/accounts/lucid_store/shared.d.ts +0 -7
- package/build/src/accounts/lucid_store/shared.js +0 -14
- package/build/src/accounts/lucid_stores.d.ts +1 -1
- package/build/src/adapters/redis_adapter.js +12 -2
- package/build/src/console_authorize.d.ts +56 -0
- package/build/src/console_authorize.js +85 -0
- package/build/src/define_config.d.ts +26 -14
- package/build/src/define_config.js +5 -3
- package/build/src/doctor/checks.d.ts +10 -5
- package/build/src/doctor/checks.js +52 -8
- package/build/src/host/account_api/account_api_controller.d.ts +2 -1
- package/build/src/host/account_api/account_api_controller.js +5 -11
- package/build/src/host/admin_api/api_clients_controller.d.ts +9 -3
- package/build/src/host/admin_api/dto.d.ts +3 -1
- package/build/src/host/admin_api/dto.js +3 -1
- package/build/src/host/admin_clients_service.d.ts +13 -5
- package/build/src/host/admin_clients_service.js +28 -6
- package/build/src/host/admin_console/admin_shell_controller.d.ts +16 -1
- package/build/src/host/admin_console/admin_shell_controller.js +21 -7
- package/build/src/host/admin_console/console_clients_controller.d.ts +21 -2
- package/build/src/host/admin_console/console_clients_controller.js +9 -0
- package/build/src/host/admin_console/console_impersonation_controller.js +3 -8
- package/build/src/host/admin_console/console_overview_controller.d.ts +2 -2
- package/build/src/host/admin_console/console_overview_controller.js +4 -2
- package/build/src/host/admin_console_warning.d.ts +8 -0
- package/build/src/host/admin_console_warning.js +18 -0
- package/build/src/host/admin_validators.d.ts +15 -11
- package/build/src/host/admin_validators.js +16 -9
- package/build/src/host/auth_host_config.d.ts +5 -0
- package/build/src/host/client_names.d.ts +9 -2
- package/build/src/host/client_names.js +11 -3
- package/build/src/host/consent_params.d.ts +7 -0
- package/build/src/host/consent_params.js +27 -0
- package/build/src/host/console_denied_page.d.ts +6 -0
- package/build/src/host/console_denied_page.js +18 -0
- package/build/src/host/console_sso.d.ts +24 -0
- package/build/src/host/console_sso.js +70 -0
- package/build/src/host/controllers/account_apps_controller.js +4 -3
- package/build/src/host/controllers/account_confirm_controller.d.ts +0 -7
- package/build/src/host/controllers/account_confirm_controller.js +0 -6
- package/build/src/host/controllers/account_orgs_controller.d.ts +0 -50
- package/build/src/host/controllers/account_orgs_controller.js +7 -113
- package/build/src/host/controllers/account_security_controller.js +7 -11
- package/build/src/host/controllers/account_session_controller.js +10 -1
- package/build/src/host/controllers/agent_consent_controller.js +23 -5
- package/build/src/host/controllers/console_sso_controller.d.ts +12 -0
- package/build/src/host/controllers/console_sso_controller.js +61 -0
- package/build/src/host/controllers/interaction_controller.js +9 -3
- package/build/src/host/controllers/poppy_consent_controller.js +33 -7
- package/build/src/host/controllers/registration_controller.js +7 -2
- package/build/src/host/i18n.d.ts +0 -4
- package/build/src/host/i18n.js +0 -6
- package/build/src/host/impersonation.d.ts +7 -15
- package/build/src/host/impersonation.js +7 -16
- package/build/src/host/interaction_recovery.js +4 -1
- package/build/src/host/login_attempt.js +5 -1
- package/build/src/host/middleware/account_auth.js +6 -0
- package/build/src/host/rate_limit.js +5 -1
- package/build/src/host/register_auth_host.d.ts +8 -0
- package/build/src/host/register_auth_host.js +116 -11
- package/build/src/host/renderers/inertia_renderer.d.ts +9 -21
- package/build/src/host/renderers/inertia_renderer.js +5 -6
- package/build/src/host/screen_props.d.ts +309 -0
- package/build/src/host/screen_props.js +11 -0
- package/build/src/host/sudo_mode.d.ts +5 -11
- package/build/src/host/sudo_mode.js +5 -11
- package/build/src/host/ui-dist/assets/index-D0EIeyKK.js +67 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/password/password_manager.js +5 -0
- package/build/src/provider/build_provider.js +6 -34
- package/build/src/provider/oidc_service.d.ts +0 -2
- package/build/src/provider/oidc_service.js +1 -12
- package/build/src/provider/token_exchange.d.ts +1 -3
- package/build/src/provider/token_exchange.js +1 -1
- package/build/stubs/config/authkit_react.stub +3 -3
- package/package.json +12 -11
- package/skills/authkit-interactions/SKILL.md +32 -5
- package/stubs/config/authkit_react.stub +3 -3
- package/build/src/commands/import_clients.d.ts +0 -40
- package/build/src/commands/import_clients.js +0 -81
- package/build/src/host/ui-dist/assets/index-DUVBDAyh.js +0 -67
- package/build/src/mixins/with_mfa.d.ts +0 -35
- package/build/src/mixins/with_mfa.js +0 -21
|
@@ -44,7 +44,11 @@ export default class PoppyConsentController {
|
|
|
44
44
|
return redirectExact(ctx.response, outcome.url);
|
|
45
45
|
if (outcome.kind === 'error') {
|
|
46
46
|
ctx.response.status(400);
|
|
47
|
-
return render(ctx, 'agents/done', {
|
|
47
|
+
return render(ctx, 'agents/done', {
|
|
48
|
+
status: 'invalid',
|
|
49
|
+
agentName: null,
|
|
50
|
+
scopes: [],
|
|
51
|
+
});
|
|
48
52
|
}
|
|
49
53
|
const account = await service.config.accountStore.findById(accountId);
|
|
50
54
|
return render(ctx, 'agents/consent', {
|
|
@@ -72,7 +76,11 @@ export default class PoppyConsentController {
|
|
|
72
76
|
const cfg = service.config;
|
|
73
77
|
const render = cfg.render;
|
|
74
78
|
if (impersonationState(ctx).active) {
|
|
75
|
-
return render(ctx, 'agents/done', {
|
|
79
|
+
return render(ctx, 'agents/done', {
|
|
80
|
+
status: 'expired',
|
|
81
|
+
agentName: null,
|
|
82
|
+
scopes: [],
|
|
83
|
+
});
|
|
76
84
|
}
|
|
77
85
|
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
78
86
|
const decided = await poppy.decideAuthorization({
|
|
@@ -82,7 +90,11 @@ export default class PoppyConsentController {
|
|
|
82
90
|
scopes: chosenScopes(ctx),
|
|
83
91
|
});
|
|
84
92
|
if (!decided) {
|
|
85
|
-
return render(ctx, 'agents/done', {
|
|
93
|
+
return render(ctx, 'agents/done', {
|
|
94
|
+
status: 'expired',
|
|
95
|
+
agentName: null,
|
|
96
|
+
scopes: [],
|
|
97
|
+
});
|
|
86
98
|
}
|
|
87
99
|
await cfg.audit?.record({
|
|
88
100
|
type: decided.approved ? 'agent.delegation_approved' : 'agent.delegation_denied',
|
|
@@ -110,14 +122,20 @@ export default class PoppyConsentController {
|
|
|
110
122
|
error: null,
|
|
111
123
|
};
|
|
112
124
|
if (impersonationState(ctx).active) {
|
|
113
|
-
return render(ctx, 'agents/consent', {
|
|
125
|
+
return render(ctx, 'agents/consent', {
|
|
126
|
+
...base,
|
|
127
|
+
error: 'agents.consent.impersonating',
|
|
128
|
+
});
|
|
114
129
|
}
|
|
115
130
|
const input = ctx.request.input('user_code');
|
|
116
131
|
if (!input)
|
|
117
132
|
return render(ctx, 'agents/consent', base);
|
|
118
133
|
const pending = await poppy.pendingDevice(input);
|
|
119
134
|
if (!pending) {
|
|
120
|
-
return render(ctx, 'agents/consent', {
|
|
135
|
+
return render(ctx, 'agents/consent', {
|
|
136
|
+
...base,
|
|
137
|
+
error: 'agents.consent.invalid_code',
|
|
138
|
+
});
|
|
121
139
|
}
|
|
122
140
|
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
123
141
|
const account = await service.config.accountStore.findById(accountId);
|
|
@@ -143,7 +161,11 @@ export default class PoppyConsentController {
|
|
|
143
161
|
const cfg = service.config;
|
|
144
162
|
const render = cfg.render;
|
|
145
163
|
if (impersonationState(ctx).active) {
|
|
146
|
-
return render(ctx, 'agents/done', {
|
|
164
|
+
return render(ctx, 'agents/done', {
|
|
165
|
+
status: 'expired',
|
|
166
|
+
agentName: null,
|
|
167
|
+
scopes: [],
|
|
168
|
+
});
|
|
147
169
|
}
|
|
148
170
|
const accountId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
149
171
|
const userCode = String(ctx.request.input('user_code') ?? '');
|
|
@@ -155,7 +177,11 @@ export default class PoppyConsentController {
|
|
|
155
177
|
scopes: chosenScopes(ctx),
|
|
156
178
|
});
|
|
157
179
|
if (!decided || !pending) {
|
|
158
|
-
return render(ctx, 'agents/done', {
|
|
180
|
+
return render(ctx, 'agents/done', {
|
|
181
|
+
status: 'expired',
|
|
182
|
+
agentName: null,
|
|
183
|
+
scopes: [],
|
|
184
|
+
});
|
|
159
185
|
}
|
|
160
186
|
await cfg.audit?.record({
|
|
161
187
|
type: decided.approved ? 'agent.delegation_approved' : 'agent.delegation_denied',
|
|
@@ -450,7 +450,10 @@ export default class AuthRegistrationController {
|
|
|
450
450
|
const cfg = service.config;
|
|
451
451
|
const render = cfg.render;
|
|
452
452
|
const token = ctx.request.qs().token ?? '';
|
|
453
|
-
return render(ctx, 'reset', {
|
|
453
|
+
return render(ctx, 'reset', {
|
|
454
|
+
token,
|
|
455
|
+
csrfToken: ctx.request.csrfToken,
|
|
456
|
+
});
|
|
454
457
|
}
|
|
455
458
|
/** POST /auth/reset-password — redefine a senha. */
|
|
456
459
|
async reset(ctx) {
|
|
@@ -552,6 +555,8 @@ export default class AuthRegistrationController {
|
|
|
552
555
|
ip: ctx.request.ip?.() ?? null,
|
|
553
556
|
});
|
|
554
557
|
}
|
|
555
|
-
return render(ctx, 'verify-email', {
|
|
558
|
+
return render(ctx, 'verify-email', {
|
|
559
|
+
verified: ok,
|
|
560
|
+
});
|
|
556
561
|
}
|
|
557
562
|
}
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -346,7 +346,6 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
346
346
|
'admin.clients.grants': string;
|
|
347
347
|
'admin.clients.redirect_uris': string;
|
|
348
348
|
'admin.clients.dynamic_notice': string;
|
|
349
|
-
'admin.clients.static_section': string;
|
|
350
349
|
'admin.clients.dynamic_section': string;
|
|
351
350
|
'admin.clients.dynamic_empty': string;
|
|
352
351
|
'admin.clients.dynamic_not_supported': string;
|
|
@@ -377,7 +376,6 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
377
376
|
'admin.clients.field_backchannel_uri_help': string;
|
|
378
377
|
'admin.clients.field_backchannel_session_required': string;
|
|
379
378
|
'admin.clients.field_backchannel_session_required_help': string;
|
|
380
|
-
'admin.clients.static_deprecated_notice': string;
|
|
381
379
|
'admin.orgs.page_title': string;
|
|
382
380
|
'admin.orgs.title': string;
|
|
383
381
|
'admin.orgs.detail_title': string;
|
|
@@ -1141,7 +1139,6 @@ export declare const PT_BR_MESSAGES: {
|
|
|
1141
1139
|
'admin.clients.grants': string;
|
|
1142
1140
|
'admin.clients.redirect_uris': string;
|
|
1143
1141
|
'admin.clients.dynamic_notice': string;
|
|
1144
|
-
'admin.clients.static_section': string;
|
|
1145
1142
|
'admin.clients.dynamic_section': string;
|
|
1146
1143
|
'admin.clients.dynamic_empty': string;
|
|
1147
1144
|
'admin.clients.dynamic_not_supported': string;
|
|
@@ -1172,7 +1169,6 @@ export declare const PT_BR_MESSAGES: {
|
|
|
1172
1169
|
'admin.clients.field_backchannel_uri_help': string;
|
|
1173
1170
|
'admin.clients.field_backchannel_session_required': string;
|
|
1174
1171
|
'admin.clients.field_backchannel_session_required_help': string;
|
|
1175
|
-
'admin.clients.static_deprecated_notice': string;
|
|
1176
1172
|
'admin.audit.page_title': string;
|
|
1177
1173
|
'admin.audit.title': string;
|
|
1178
1174
|
'admin.audit.type_placeholder': string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -367,7 +367,6 @@ export const DEFAULT_MESSAGES = {
|
|
|
367
367
|
'admin.clients.grants': 'Grants: {grants}',
|
|
368
368
|
'admin.clients.redirect_uris': 'Redirects: {uris}',
|
|
369
369
|
'admin.clients.dynamic_notice': 'Dynamic client registration (RFC 7591) is on — clients registered via /reg are persisted in the adapter and appear in the dynamic section below.',
|
|
370
|
-
'admin.clients.static_section': 'Static clients (config)',
|
|
371
370
|
'admin.clients.dynamic_section': 'Dynamic clients (adapter)',
|
|
372
371
|
'admin.clients.dynamic_empty': 'No dynamic clients persisted.',
|
|
373
372
|
'admin.clients.dynamic_not_supported': 'The configured OIDC adapter does not support client enumeration — dynamic management is unavailable.',
|
|
@@ -398,8 +397,6 @@ export const DEFAULT_MESSAGES = {
|
|
|
398
397
|
'admin.clients.field_backchannel_uri_help': 'OIDC Back-Channel Logout endpoint of the RP. Leave blank if not needed.',
|
|
399
398
|
'admin.clients.field_backchannel_session_required': 'Require sid in logout_token',
|
|
400
399
|
'admin.clients.field_backchannel_session_required_help': 'When checked, the IdP includes the session ID (sid) in every logout_token sent to this client.',
|
|
401
|
-
'admin.clients.static_deprecated_notice': 'These clients are defined statically in config and are deprecated. ' +
|
|
402
|
-
'Migrate them to the adapter/DB with:',
|
|
403
400
|
// Console admin — organizations.
|
|
404
401
|
'admin.orgs.page_title': 'Organizations',
|
|
405
402
|
'admin.orgs.title': 'Organizations',
|
|
@@ -1250,7 +1247,6 @@ export const PT_BR_MESSAGES = {
|
|
|
1250
1247
|
'admin.clients.grants': 'Grants: {grants}',
|
|
1251
1248
|
'admin.clients.redirect_uris': 'Redirects: {uris}',
|
|
1252
1249
|
'admin.clients.dynamic_notice': 'O registro dinâmico de clients (RFC 7591) está ligado — clients registrados via /reg são persistidos no adapter e aparecem na seção dinâmica abaixo.',
|
|
1253
|
-
'admin.clients.static_section': 'Clients estáticos (config)',
|
|
1254
1250
|
'admin.clients.dynamic_section': 'Clients dinâmicos (adapter)',
|
|
1255
1251
|
'admin.clients.dynamic_empty': 'Nenhum client dinâmico persistido.',
|
|
1256
1252
|
'admin.clients.dynamic_not_supported': 'O adapter OIDC configurado não suporta enumeração de clients — a gestão dinâmica fica indisponível.',
|
|
@@ -1281,8 +1277,6 @@ export const PT_BR_MESSAGES = {
|
|
|
1281
1277
|
'admin.clients.field_backchannel_uri_help': 'Endpoint de OIDC Back-Channel Logout do RP. Deixe em branco se não usar.',
|
|
1282
1278
|
'admin.clients.field_backchannel_session_required': 'Exigir sid no logout_token',
|
|
1283
1279
|
'admin.clients.field_backchannel_session_required_help': 'Quando marcado, o IdP inclui o session ID (sid) em cada logout_token enviado a este client.',
|
|
1284
|
-
'admin.clients.static_deprecated_notice': 'Estes clients estão definidos estaticamente no config e estão depreciados. ' +
|
|
1285
|
-
'Migre-os para o adapter/DB com:',
|
|
1286
1280
|
// Console admin — auditoria.
|
|
1287
1281
|
'admin.audit.page_title': 'Auditoria',
|
|
1288
1282
|
'admin.audit.title': 'Log de auditoria',
|
|
@@ -18,16 +18,13 @@ export interface ImpersonationPanel {
|
|
|
18
18
|
curl: string;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Um client candidato a hospedar o token-exchange
|
|
22
|
-
*
|
|
23
|
-
* que trazem o secret) quanto os clients de RUNTIME lidos do adapter pelo
|
|
24
|
-
* `AdminClientsService` (que NÃO trazem o secret — ele não é recuperável).
|
|
21
|
+
* Um client candidato a hospedar o token-exchange, na forma que o
|
|
22
|
+
* `AdminClientsService` lê do adapter (sem o secret — ele não é recuperável).
|
|
25
23
|
*/
|
|
26
24
|
export interface ImpersonationClientLike {
|
|
27
25
|
clientId: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/** Runtime clients reportam confidencialidade sem expor o secret. */
|
|
26
|
+
grantTypes?: string[];
|
|
27
|
+
/** Client confidencial: o curl sai com o placeholder do secret. */
|
|
31
28
|
confidential?: boolean;
|
|
32
29
|
}
|
|
33
30
|
/**
|
|
@@ -35,12 +32,7 @@ export interface ImpersonationClientLike {
|
|
|
35
32
|
* client candidato tem o grant token-exchange habilitado (sem ele o fluxo não
|
|
36
33
|
* funciona) — a UI então mostra a instrução de habilitar o grant.
|
|
37
34
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* e o config resolvido nunca os carrega — é o que o `checkClients` do doctor
|
|
41
|
-
* afirma. Passar só o config faria o painel devolver `null` SEMPRE, que foi
|
|
42
|
-
* exatamente o bug: o botão do console nunca teve um client para oferecer.
|
|
43
|
-
* O fallback para `cfg.clients` existe só para hosts que ainda declaram clients
|
|
44
|
-
* estáticos e para os testes que os declaram.
|
|
35
|
+
* `clients` são os clients do adapter (console admin / Admin API /
|
|
36
|
+
* `authkit:clients:create`), por exemplo `new AdminClientsService(service).list()`.
|
|
45
37
|
*/
|
|
46
|
-
export declare function buildImpersonationPanel(cfg: Pick<ResolvedServerConfig, 'issuer'
|
|
38
|
+
export declare function buildImpersonationPanel(cfg: Pick<ResolvedServerConfig, 'issuer'>, targetId: string, clients: readonly ImpersonationClientLike[]): ImpersonationPanel | null;
|
|
@@ -7,27 +7,18 @@ const SECRET_PLACEHOLDER = '<CLIENT_SECRET>';
|
|
|
7
7
|
* client candidato tem o grant token-exchange habilitado (sem ele o fluxo não
|
|
8
8
|
* funciona) — a UI então mostra a instrução de habilitar o grant.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* e o config resolvido nunca os carrega — é o que o `checkClients` do doctor
|
|
13
|
-
* afirma. Passar só o config faria o painel devolver `null` SEMPRE, que foi
|
|
14
|
-
* exatamente o bug: o botão do console nunca teve um client para oferecer.
|
|
15
|
-
* O fallback para `cfg.clients` existe só para hosts que ainda declaram clients
|
|
16
|
-
* estáticos e para os testes que os declaram.
|
|
10
|
+
* `clients` são os clients do adapter (console admin / Admin API /
|
|
11
|
+
* `authkit:clients:create`), por exemplo `new AdminClientsService(service).list()`.
|
|
17
12
|
*/
|
|
18
13
|
export function buildImpersonationPanel(cfg, targetId, clients) {
|
|
19
|
-
const
|
|
20
|
-
const client = candidates.find((c) => (c.grants ?? []).includes(TOKEN_EXCHANGE));
|
|
14
|
+
const client = clients.find((c) => (c.grantTypes ?? []).includes(TOKEN_EXCHANGE));
|
|
21
15
|
if (!client)
|
|
22
16
|
return null;
|
|
23
17
|
const tokenEndpoint = `${cfg.issuer.replace(/\/+$/, '')}/token`;
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const confidential = client.confidential ?? client.clientSecret !== undefined;
|
|
29
|
-
const auth = confidential
|
|
30
|
-
? ` -u '${client.clientId}:${client.clientSecret ?? SECRET_PLACEHOLDER}' \\\n`
|
|
18
|
+
// O secret NÃO é recuperável (a lib mostra o secret uma única vez na criação),
|
|
19
|
+
// então o curl de um client confidencial sai com um placeholder.
|
|
20
|
+
const auth = client.confidential
|
|
21
|
+
? ` -u '${client.clientId}:${SECRET_PLACEHOLDER}' \\\n`
|
|
31
22
|
: ` -d 'client_id=${client.clientId}' \\\n`;
|
|
32
23
|
const curl = `curl -X POST '${tokenEndpoint}' \\\n${auth} -d 'grant_type=${TOKEN_EXCHANGE}' \\\n -d 'subject_token=<ADMIN_ACCESS_TOKEN>' \\\n -d 'subject_token_type=${ACCESS_TOKEN_TYPE}' \\\n -d 'requested_subject=${targetId}' \\\n -d 'scope=openid profile email'`;
|
|
33
24
|
return {
|
|
@@ -87,7 +87,10 @@ export async function recoverLostInteraction(ctx) {
|
|
|
87
87
|
return ctx.response.redirect(loginUrl);
|
|
88
88
|
}
|
|
89
89
|
ctx.response.status(400);
|
|
90
|
-
const body = await render(ctx, 'session-expired', {
|
|
90
|
+
const body = await render(ctx, 'session-expired', {
|
|
91
|
+
loginUrl,
|
|
92
|
+
brand,
|
|
93
|
+
});
|
|
91
94
|
// ESCREVE o body na resposta. Diferente do dispatch normal de rota — onde o
|
|
92
95
|
// `useReturnValue`/`canWriteResponseBody` do http-server envia o valor
|
|
93
96
|
// retornado — o caminho de exception handler DESCARTA o retorno de
|
|
@@ -231,7 +231,11 @@ export async function attemptPasswordLogin(cfg, input) {
|
|
|
231
231
|
if (lock.locked) {
|
|
232
232
|
return { ok: false, locked: true, retryAfterSec: lock.retryAfterSec };
|
|
233
233
|
}
|
|
234
|
-
|
|
234
|
+
// A form posted without a password (e-mail only, typical when password login is
|
|
235
|
+
// off) is a failed attempt like a wrong password, never a crash in the hasher.
|
|
236
|
+
const account = typeof password === 'string' && password !== ''
|
|
237
|
+
? await cfg.accountStore.verifyCredentials(email, password)
|
|
238
|
+
: null;
|
|
235
239
|
if (!account) {
|
|
236
240
|
// `clientId` só entra no evento quando o fluxo o fornece (interaction).
|
|
237
241
|
await cfg.audit?.record(input.clientId !== undefined
|
|
@@ -7,6 +7,12 @@ export default class AccountAuthMiddleware {
|
|
|
7
7
|
async handle(ctx, next) {
|
|
8
8
|
// Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
|
|
9
9
|
if (!(await ensureConsoleSession(ctx))) {
|
|
10
|
+
// JSON callers get a status, not an HTML login page.
|
|
11
|
+
if (ctx.request?.accepts?.(['html', 'json']) === 'json') {
|
|
12
|
+
return ctx.response
|
|
13
|
+
.status(401)
|
|
14
|
+
.send({ error: { code: 'unauthenticated', message: 'Sign in to continue.' } });
|
|
15
|
+
}
|
|
10
16
|
// Destino configurável (`accountLoginUrl`): default `/account/login`.
|
|
11
17
|
return ctx.response.redirect(getAccountLoginUrl());
|
|
12
18
|
}
|
|
@@ -159,7 +159,11 @@ export function withFriendly429(throttle) {
|
|
|
159
159
|
ctx.response.status(429);
|
|
160
160
|
if (retryAfter)
|
|
161
161
|
ctx.response.header('Retry-After', String(retryAfter));
|
|
162
|
-
const body = await render(ctx, 'throttled', {
|
|
162
|
+
const body = await render(ctx, 'throttled', {
|
|
163
|
+
brand,
|
|
164
|
+
retryAfter,
|
|
165
|
+
retryUrl,
|
|
166
|
+
});
|
|
163
167
|
const res = ctx.response;
|
|
164
168
|
if (body !== undefined && !res.hasLazyBody && body !== ctx.response) {
|
|
165
169
|
res.send(body);
|
|
@@ -5,6 +5,12 @@ import type { PolicyRouteOption } from './config_locks.js';
|
|
|
5
5
|
import type { SudoMethod } from './sudo/types.js';
|
|
6
6
|
/** Chave da sessão Adonis que registra o timestamp da última atividade (idle timeout). */
|
|
7
7
|
export declare const ACCOUNT_LAST_SEEN_KEY = "authkit_last_seen";
|
|
8
|
+
/**
|
|
9
|
+
* `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
|
|
10
|
+
* as `fetch` from the React client sends): a JSON API answers it with a status,
|
|
11
|
+
* not with a redirect to an HTML login page.
|
|
12
|
+
*/
|
|
13
|
+
export declare function wantsJson(ctx: any): boolean;
|
|
8
14
|
/**
|
|
9
15
|
* Guard do console admin (B6). Como o `accountGuard`, é uma closure inline (forma
|
|
10
16
|
* confiável do `.use()` num grupo). Exige:
|
|
@@ -14,6 +20,8 @@ export declare const ACCOUNT_LAST_SEEN_KEY = "authkit_last_seen";
|
|
|
14
20
|
* (senão → `accountHome(cfg)`, default /account/security — NÃO revela a
|
|
15
21
|
* existência do /admin, e cai numa tela que o host controla via `accountHome`;
|
|
16
22
|
* se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
|
|
23
|
+
* 3. `config.admin.authorize` liberando (403/401/redirect conforme o resultado). Omitido:
|
|
24
|
+
* aberto fora de produção, 403 em produção.
|
|
17
25
|
* As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
|
|
18
26
|
*/
|
|
19
27
|
export declare const adminGuard: (ctx: any, next: () => Promise<void>) => Promise<any>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getBootedApp } from '../../services/booted_app.js';
|
|
2
|
+
import { resolveConsoleAuthorize, } from '../console_authorize.js';
|
|
1
3
|
import { resolveRateLimit } from '../define_config.js';
|
|
2
4
|
import { accountHome } from './account_home.js';
|
|
3
5
|
import { getAccountLoginUrl, setAccountLoginUrl } from './account_login_url.js';
|
|
@@ -7,7 +9,9 @@ import { ACCOUNT_SESSION_KEY } from './account_session_key.js';
|
|
|
7
9
|
import { adminApiGuard } from './admin_api/admin_api_guard.js';
|
|
8
10
|
import { normalizeAdminApiPrefix, normalizeAdminPrefix, setAdminApiPrefix, setAdminPrefix, } from './admin_prefix.js';
|
|
9
11
|
import { getAuthHostConfig, markAuthHostAutoMounted, wasAuthHostAutoMounted, } from './auth_host_config.js';
|
|
10
|
-
import {
|
|
12
|
+
import { consoleDeniedPage } from './console_denied_page.js';
|
|
13
|
+
import { CONSOLE_SSO_CALLBACK_PATH, CONSOLE_SSO_START_PATH } from './console_sso.js';
|
|
14
|
+
import { endBridgedIdpSession, ensureConsoleSession } from './idp_session_bridge.js';
|
|
11
15
|
import { POPPY_WEB_SESSION_KEY } from './poppy_session_key.js';
|
|
12
16
|
import { createAuthThrottles } from './rate_limit.js';
|
|
13
17
|
import { redirectExact } from './redirect_exact.js';
|
|
@@ -67,7 +71,10 @@ async function checkAndRefreshIdle(ctx) {
|
|
|
67
71
|
const lastSeen = ctx.session?.get(ACCOUNT_LAST_SEEN_KEY);
|
|
68
72
|
const now = Date.now();
|
|
69
73
|
if (lastSeen !== undefined && now - lastSeen > idleMs) {
|
|
70
|
-
// Idle excedido: encerra a sessão.
|
|
74
|
+
// Idle excedido: encerra a sessão. Console aberto pela sessão do IdP (SSO):
|
|
75
|
+
// encerra também essa sessão, senão a ponte reabriria o console no próximo
|
|
76
|
+
// request e o idle timeout não teria efeito.
|
|
77
|
+
await endBridgedIdpSession(ctx);
|
|
71
78
|
ctx.session?.forget(ACCOUNT_SESSION_KEY);
|
|
72
79
|
ctx.session?.forget(ACCOUNT_LAST_SEEN_KEY);
|
|
73
80
|
return true;
|
|
@@ -119,6 +126,28 @@ function isPoppyBrowser(ctx) {
|
|
|
119
126
|
const marker = ctx.session?.get?.(POPPY_WEB_SESSION_KEY);
|
|
120
127
|
return typeof marker === 'object' && marker !== null && typeof marker.sessionId === 'string';
|
|
121
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
|
|
131
|
+
* as `fetch` from the React client sends): a JSON API answers it with a status,
|
|
132
|
+
* not with a redirect to an HTML login page.
|
|
133
|
+
*/
|
|
134
|
+
export function wantsJson(ctx) {
|
|
135
|
+
try {
|
|
136
|
+
return ctx.request?.accepts?.(['html', 'json']) === 'json';
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** 401 for a JSON caller, otherwise the redirect to the login. */
|
|
143
|
+
function unauthenticated(ctx, redirectTo) {
|
|
144
|
+
if (wantsJson(ctx)) {
|
|
145
|
+
return ctx.response
|
|
146
|
+
.status(401)
|
|
147
|
+
.send({ error: { code: 'unauthenticated', message: 'Sign in to continue.' } });
|
|
148
|
+
}
|
|
149
|
+
return redirectExact(ctx.response, redirectTo);
|
|
150
|
+
}
|
|
122
151
|
const accountGuard = async (ctx, next) => {
|
|
123
152
|
// O navegador de um personal agent Poppy (§5) não entra no console de conta nem
|
|
124
153
|
// nas telas de consentimento: a sessão é do agente, não do usuário.
|
|
@@ -126,12 +155,12 @@ const accountGuard = async (ctx, next) => {
|
|
|
126
155
|
return ctx.response.status(403).send('');
|
|
127
156
|
// Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
|
|
128
157
|
if (!(await ensureConsoleSession(ctx))) {
|
|
129
|
-
return
|
|
158
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx));
|
|
130
159
|
}
|
|
131
160
|
// Idle timeout: encerra e redireciona com query param de motivo.
|
|
132
161
|
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
133
162
|
if (idleExpired) {
|
|
134
|
-
return
|
|
163
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
|
|
135
164
|
}
|
|
136
165
|
return next();
|
|
137
166
|
};
|
|
@@ -144,6 +173,8 @@ const accountGuard = async (ctx, next) => {
|
|
|
144
173
|
* (senão → `accountHome(cfg)`, default /account/security — NÃO revela a
|
|
145
174
|
* existência do /admin, e cai numa tela que o host controla via `accountHome`;
|
|
146
175
|
* se a tela default estiver desmontada, aponte `config.accountHome` para uma montada).
|
|
176
|
+
* 3. `config.admin.authorize` liberando (403/401/redirect conforme o resultado). Omitido:
|
|
177
|
+
* aberto fora de produção, 403 em produção.
|
|
147
178
|
* As roles permitidas são resolvidas em runtime do `authkit.server` (config lazy).
|
|
148
179
|
*/
|
|
149
180
|
export const adminGuard = async (ctx, next) => {
|
|
@@ -162,12 +193,12 @@ export const adminGuard = async (ctx, next) => {
|
|
|
162
193
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
163
194
|
if (!accountId) {
|
|
164
195
|
// `/account/login` é sempre o login da conta — NÃO muda com o prefixo admin.
|
|
165
|
-
return
|
|
196
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx));
|
|
166
197
|
}
|
|
167
198
|
// Idle timeout: também protege o console admin.
|
|
168
199
|
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
169
200
|
if (idleExpired) {
|
|
170
|
-
return
|
|
201
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
|
|
171
202
|
}
|
|
172
203
|
const allowed = cfg.admin.roles;
|
|
173
204
|
const account = await cfg.accountStore.findById(accountId);
|
|
@@ -179,10 +210,68 @@ export const adminGuard = async (ctx, next) => {
|
|
|
179
210
|
if (!isAdmin) {
|
|
180
211
|
// Evita vazar a existência do console admin: redireciona para o accountHome
|
|
181
212
|
// (sem mostrar a URL do admin).
|
|
182
|
-
|
|
213
|
+
if (wantsJson(ctx)) {
|
|
214
|
+
return ctx.response
|
|
215
|
+
.status(403)
|
|
216
|
+
.send({ error: { code: 'forbidden', message: 'Admin role required.' } });
|
|
217
|
+
}
|
|
218
|
+
return redirectExact(ctx.response, accountHome(cfg));
|
|
183
219
|
}
|
|
220
|
+
// `admin.authorize`, after the roles: both must pass. Omitted, the console is open
|
|
221
|
+
// outside production and closed (403) in production.
|
|
222
|
+
const decision = cfg.admin.authorize
|
|
223
|
+
? await adminAuthorizeChecker(cfg.admin.authorize)(ctx)
|
|
224
|
+
: appInProduction()
|
|
225
|
+
? { allowed: false, status: 403 }
|
|
226
|
+
: { allowed: true };
|
|
227
|
+
if (!decision.allowed)
|
|
228
|
+
return consoleDenied(ctx, decision);
|
|
184
229
|
return next();
|
|
185
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* Whether the app runs in production. Unknown (the provider did not register the
|
|
233
|
+
* app) counts as production, so an omitted `admin.authorize` fails closed.
|
|
234
|
+
*/
|
|
235
|
+
function appInProduction() {
|
|
236
|
+
try {
|
|
237
|
+
return getBootedApp().inProduction !== false;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** The response for an `admin.authorize` denial: same rules in every `@adonis-agora` console. */
|
|
244
|
+
function consoleDenied(ctx, decision) {
|
|
245
|
+
const json = wantsJson(ctx);
|
|
246
|
+
if (decision.redirectTo) {
|
|
247
|
+
if (json) {
|
|
248
|
+
return ctx.response
|
|
249
|
+
.status(401)
|
|
250
|
+
.send({ error: 'unauthenticated', redirectTo: decision.redirectTo });
|
|
251
|
+
}
|
|
252
|
+
return redirectExact(ctx.response, decision.redirectTo);
|
|
253
|
+
}
|
|
254
|
+
if (decision.status === 401) {
|
|
255
|
+
if (json)
|
|
256
|
+
return ctx.response.status(401).send({ error: 'unauthenticated' });
|
|
257
|
+
ctx.response.header('Content-Type', 'text/html; charset=utf-8');
|
|
258
|
+
return ctx.response.status(401).send(consoleDeniedPage(401));
|
|
259
|
+
}
|
|
260
|
+
if (json)
|
|
261
|
+
return ctx.response.status(403).send({ error: 'forbidden' });
|
|
262
|
+
ctx.response.header('Content-Type', 'text/html; charset=utf-8');
|
|
263
|
+
return ctx.response.status(403).send(consoleDeniedPage(403));
|
|
264
|
+
}
|
|
265
|
+
/** One checker per configured `admin.authorize`, so a lazy import runs once. */
|
|
266
|
+
const adminAuthorizeCheckers = new WeakMap();
|
|
267
|
+
function adminAuthorizeChecker(authorize) {
|
|
268
|
+
let checker = adminAuthorizeCheckers.get(authorize);
|
|
269
|
+
if (!checker) {
|
|
270
|
+
checker = resolveConsoleAuthorize(authorize);
|
|
271
|
+
adminAuthorizeCheckers.set(authorize, checker);
|
|
272
|
+
}
|
|
273
|
+
return checker;
|
|
274
|
+
}
|
|
186
275
|
const C = {
|
|
187
276
|
oidc: () => import('../controllers/oidc_callback_controller.js'),
|
|
188
277
|
authorizationServerMetadata: () => import('../controllers/authorization_server_metadata_controller.js'),
|
|
@@ -191,6 +280,7 @@ const C = {
|
|
|
191
280
|
social: () => import('./controllers/social_controller.js'),
|
|
192
281
|
patIntrospection: () => import('./controllers/pat_introspection_controller.js'),
|
|
193
282
|
accountSession: () => import('./controllers/account_session_controller.js'),
|
|
283
|
+
consoleSso: () => import('./controllers/console_sso_controller.js'),
|
|
194
284
|
accountTokens: () => import('./controllers/account_tokens_controller.js'),
|
|
195
285
|
accountSecurity: () => import('./controllers/account_security_controller.js'),
|
|
196
286
|
accountApps: () => import('./controllers/account_apps_controller.js'),
|
|
@@ -353,6 +443,17 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
353
443
|
if (accountLoginUrlOpt !== undefined) {
|
|
354
444
|
setAccountLoginUrl(accountLoginUrlOpt);
|
|
355
445
|
}
|
|
446
|
+
// Console sem porta de entrada: SSO desligado (`acceptIdpSession: false`) deixa só o
|
|
447
|
+
// login próprio do console, que é por senha; com a senha desligada ninguém entra em
|
|
448
|
+
// `/account/*` nem no console admin. Só passa com um `accountLoginUrl` do host (aqui
|
|
449
|
+
// ou em `routes.accountLoginUrl`) — por isso a checagem é no registro, não no config.
|
|
450
|
+
if (hostCfg?.consoleNeedsLoginUrl && !accountLoginUrlOpt) {
|
|
451
|
+
throw new Error('authkit: `accountSession.acceptIdpSession: false` com `authMethods: { password: false }` ' +
|
|
452
|
+
'deixa o console de conta e o console admin sem nenhum jeito de entrar (o login próprio ' +
|
|
453
|
+
'do console é por senha). Remova `acceptIdpSession: false` (o default entra pelo login ' +
|
|
454
|
+
'do IdP) ou passe `accountLoginUrl` (no `registerAuthHost` ou em `routes`) apontando ' +
|
|
455
|
+
'para um login do seu app.');
|
|
456
|
+
}
|
|
356
457
|
// Montagem por tela do console de conta. `undefined` → tudo montado;
|
|
357
458
|
// `false` → nada; objeto → cada flag ausente default `true`.
|
|
358
459
|
const accountOpt = opts.account ?? routesCfg?.account;
|
|
@@ -479,6 +580,13 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
479
580
|
// Provider OIDC (wildcard + root) — o que registerOidcRoutes fazia.
|
|
480
581
|
router.any(`${mount}/*`, [C.oidc]).as('authkit.oidc.wildcard');
|
|
481
582
|
router.any(mount, [C.oidc]).as('authkit.oidc.root');
|
|
583
|
+
// Login do console pelo IdP (SSO; client interno `authkit-console`, ver
|
|
584
|
+
// `console_sso.ts`). Sempre montado: com `acceptIdpSession: false` o início só
|
|
585
|
+
// redireciona para o login próprio do console.
|
|
586
|
+
router.get(CONSOLE_SSO_START_PATH, [C.consoleSso, 'start']).as('authkit.console_sso.start');
|
|
587
|
+
router
|
|
588
|
+
.get(CONSOLE_SSO_CALLBACK_PATH, [C.consoleSso, 'callback'])
|
|
589
|
+
.as('authkit.console_sso.callback');
|
|
482
590
|
// Interaction (login multi-step + consent + signup).
|
|
483
591
|
// Cada rota passa por `keep(key, …)`: entra em `AuthHostRouteMap.routes`/`urls` e
|
|
484
592
|
// recebe o `extraMiddleware` do host (depois do throttle da lib).
|
|
@@ -770,10 +878,6 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
770
878
|
C.accountOrgs,
|
|
771
879
|
'revokeInvitation',
|
|
772
880
|
]);
|
|
773
|
-
// JSON endpoints for React hooks (authkit-react).
|
|
774
|
-
router.get(`${orgsPath}/json`, [C.accountOrgs, 'listJson']);
|
|
775
|
-
router.get(`${orgsPath}/invitations/json`, [C.accountOrgs, 'listInvitationsJson']);
|
|
776
|
-
router.get(`${orgsPath}/:id/json`, [C.accountOrgs, 'showJson']);
|
|
777
881
|
}
|
|
778
882
|
// ─── Account self-service JSON API (authkit-react TanStack hooks) ─────
|
|
779
883
|
// ⚠️ ORDER MATTERS: fixed-segment routes before parameterised ones.
|
|
@@ -925,6 +1029,7 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
925
1029
|
// Clients OIDC.
|
|
926
1030
|
router.get(`${ap}/api/clients`, [C.consoleClients, 'index']);
|
|
927
1031
|
router.post(`${ap}/api/clients`, [C.consoleClients, 'store']);
|
|
1032
|
+
router.get(`${ap}/api/clients/:id`, [C.consoleClients, 'show']);
|
|
928
1033
|
router.patch(`${ap}/api/clients/:id`, [C.consoleClients, 'update']);
|
|
929
1034
|
router.delete(`${ap}/api/clients/:id`, [C.consoleClients, 'destroy']);
|
|
930
1035
|
router.post(`${ap}/api/clients/:id/regenerate-secret`, [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { AuthkitScreen } from '../screen_props.js';
|
|
2
3
|
/**
|
|
3
4
|
* Opções do renderer Inertia/React.
|
|
4
5
|
*/
|
|
@@ -11,18 +12,10 @@ export interface InertiaRendererOptions {
|
|
|
11
12
|
/**
|
|
12
13
|
* Lista de nomes de view que o host criou como páginas React (allowlist).
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* ### ⚠️ Atenção: sem `views`, qualquer tela nova da lib quebra hosts SSR
|
|
19
|
-
*
|
|
20
|
-
* Se `views` for omitido, o comportamento legado é preservado (todas as views
|
|
21
|
-
* são enviadas ao Inertia) para compatibilidade retroativa. Nesse modo, qualquer
|
|
22
|
-
* tela nova adicionada pela lib gerará um erro SSR no host ("Cannot read
|
|
23
|
-
* properties of undefined (reading 'default')") porque o host não terá criado a
|
|
24
|
-
* página React correspondente. **Recomenda-se sempre fornecer `views`** com
|
|
25
|
-
* exatamente as páginas que o scaffold (`node ace configure`) gerou.
|
|
15
|
+
* **Somente** as views listadas passam pelo Inertia; qualquer outra view (não
|
|
16
|
+
* listada, ou prefixada com `admin/`) é renderizada pelo renderer Edge built-in
|
|
17
|
+
* da lib. Liste exatamente as páginas que o scaffold (`node ace configure`)
|
|
18
|
+
* gerou, mais as que você criar.
|
|
26
19
|
*
|
|
27
20
|
* A ordem do array é irrelevante — `views` é tratado como um *conjunto*
|
|
28
21
|
* (allowlist). Os nomes têm autocomplete via {@link AuthkitScreen}.
|
|
@@ -38,14 +31,9 @@ export interface InertiaRendererOptions {
|
|
|
38
31
|
* ],
|
|
39
32
|
* })
|
|
40
33
|
*/
|
|
41
|
-
views
|
|
34
|
+
views: readonly AuthkitScreen[];
|
|
42
35
|
}
|
|
43
|
-
|
|
44
|
-
* Nomes das telas conhecidas da lib — dá autocomplete no array `views`.
|
|
45
|
-
* O `(string & {})` mantém o tipo aberto: telas custom (ou de versões mais
|
|
46
|
-
* novas da lib) continuam aceitas sem erro de compilação.
|
|
47
|
-
*/
|
|
48
|
-
export type AuthkitScreen = 'login' | 'signup' | 'consent' | 'forgot' | 'reset' | 'verify-email' | 'mfa-challenge' | 'otp-unlock' | 'maintenance' | 'session-expired' | 'account/login' | 'account/tokens' | 'account/mfa' | 'account/security' | 'account/apps' | 'account/orgs' | 'account/confirm' | 'account/email-confirmed' | (string & {});
|
|
36
|
+
export type { AuthkitScreen } from '../screen_props.js';
|
|
49
37
|
/**
|
|
50
38
|
* Renderer do seam para hosts Inertia/React.
|
|
51
39
|
*
|
|
@@ -53,8 +41,8 @@ export type AuthkitScreen = 'login' | 'signup' | 'consent' | 'forgot' | 'reset'
|
|
|
53
41
|
* (O console admin não passa por aqui: é a SPA self-contained da lib,
|
|
54
42
|
* servida pelo shell controller.)
|
|
55
43
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
44
|
+
* Apenas as views listadas em `views` (allowlist) vão ao Inertia; qualquer outra
|
|
45
|
+
* view é renderizada pelo Edge renderer built-in.
|
|
58
46
|
*
|
|
59
47
|
* ---
|
|
60
48
|
*
|
|
@@ -24,8 +24,8 @@ async function resolveMessagesFromCtx(ctx) {
|
|
|
24
24
|
* (O console admin não passa por aqui: é a SPA self-contained da lib,
|
|
25
25
|
* servida pelo shell controller.)
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Apenas as views listadas em `views` (allowlist) vão ao Inertia; qualquer outra
|
|
28
|
+
* view é renderizada pelo Edge renderer built-in.
|
|
29
29
|
*
|
|
30
30
|
* ---
|
|
31
31
|
*
|
|
@@ -59,11 +59,10 @@ async function resolveMessagesFromCtx(ctx) {
|
|
|
59
59
|
* ```
|
|
60
60
|
*/
|
|
61
61
|
export function inertiaRenderer(opts) {
|
|
62
|
-
const allowed =
|
|
62
|
+
const allowed = new Set(opts.views);
|
|
63
63
|
return async (ctx, view, props) => {
|
|
64
|
-
// Allowlist:
|
|
65
|
-
|
|
66
|
-
if (allowed !== null && !allowed.has(view)) {
|
|
64
|
+
// Allowlist: view fora da lista vai ao Edge (o host não tem a página React).
|
|
65
|
+
if (!allowed.has(view)) {
|
|
67
66
|
return renderEdgeView(ctx, view, props);
|
|
68
67
|
}
|
|
69
68
|
// Happy path: view vai ao Inertia.
|