@adonis-agora/authkit-server 0.90.2 → 1.0.0-rc.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/CHANGELOG.md +3637 -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 +3 -3
- package/build/index.js +3 -5
- 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/define_config.d.ts +6 -12
- package/build/src/define_config.js +3 -2
- package/build/src/doctor/checks.d.ts +3 -4
- package/build/src/doctor/checks.js +5 -7
- 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 +18 -5
- 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_validators.d.ts +15 -11
- package/build/src/host/admin_validators.js +16 -9
- 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/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/agent_consent_controller.js +23 -5
- package/build/src/host/controllers/interaction_controller.js +6 -2
- 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 +6 -0
- package/build/src/host/register_auth_host.js +32 -8
- 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 +0 -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 +9 -8
- 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
|
@@ -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:
|
|
@@ -119,6 +119,28 @@ function isPoppyBrowser(ctx) {
|
|
|
119
119
|
const marker = ctx.session?.get?.(POPPY_WEB_SESSION_KEY);
|
|
120
120
|
return typeof marker === 'object' && marker !== null && typeof marker.sessionId === 'string';
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* `true` when the caller wants JSON (an `Accept` that prefers `application/json`,
|
|
124
|
+
* as `fetch` from the React client sends): a JSON API answers it with a status,
|
|
125
|
+
* not with a redirect to an HTML login page.
|
|
126
|
+
*/
|
|
127
|
+
export function wantsJson(ctx) {
|
|
128
|
+
try {
|
|
129
|
+
return ctx.request?.accepts?.(['html', 'json']) === 'json';
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** 401 for a JSON caller, otherwise the redirect to the login. */
|
|
136
|
+
function unauthenticated(ctx, redirectTo) {
|
|
137
|
+
if (wantsJson(ctx)) {
|
|
138
|
+
return ctx.response
|
|
139
|
+
.status(401)
|
|
140
|
+
.send({ error: { code: 'unauthenticated', message: 'Sign in to continue.' } });
|
|
141
|
+
}
|
|
142
|
+
return redirectExact(ctx.response, redirectTo);
|
|
143
|
+
}
|
|
122
144
|
const accountGuard = async (ctx, next) => {
|
|
123
145
|
// O navegador de um personal agent Poppy (§5) não entra no console de conta nem
|
|
124
146
|
// nas telas de consentimento: a sessão é do agente, não do usuário.
|
|
@@ -126,12 +148,12 @@ const accountGuard = async (ctx, next) => {
|
|
|
126
148
|
return ctx.response.status(403).send('');
|
|
127
149
|
// Sessão do console — ou, com `accountSession.acceptIdpSession`, a do IdP (SSO).
|
|
128
150
|
if (!(await ensureConsoleSession(ctx))) {
|
|
129
|
-
return
|
|
151
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx));
|
|
130
152
|
}
|
|
131
153
|
// Idle timeout: encerra e redireciona com query param de motivo.
|
|
132
154
|
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
133
155
|
if (idleExpired) {
|
|
134
|
-
return
|
|
156
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
|
|
135
157
|
}
|
|
136
158
|
return next();
|
|
137
159
|
};
|
|
@@ -162,12 +184,12 @@ export const adminGuard = async (ctx, next) => {
|
|
|
162
184
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
163
185
|
if (!accountId) {
|
|
164
186
|
// `/account/login` é sempre o login da conta — NÃO muda com o prefixo admin.
|
|
165
|
-
return
|
|
187
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx));
|
|
166
188
|
}
|
|
167
189
|
// Idle timeout: também protege o console admin.
|
|
168
190
|
const idleExpired = await checkAndRefreshIdle(ctx);
|
|
169
191
|
if (idleExpired) {
|
|
170
|
-
return
|
|
192
|
+
return unauthenticated(ctx, buildLoginRedirect(ctx, 'reason=idle'));
|
|
171
193
|
}
|
|
172
194
|
const allowed = cfg.admin.roles;
|
|
173
195
|
const account = await cfg.accountStore.findById(accountId);
|
|
@@ -179,6 +201,11 @@ export const adminGuard = async (ctx, next) => {
|
|
|
179
201
|
if (!isAdmin) {
|
|
180
202
|
// Evita vazar a existência do console admin: redireciona para o accountHome
|
|
181
203
|
// (sem mostrar a URL do admin).
|
|
204
|
+
if (wantsJson(ctx)) {
|
|
205
|
+
return ctx.response
|
|
206
|
+
.status(403)
|
|
207
|
+
.send({ error: { code: 'forbidden', message: 'Admin role required.' } });
|
|
208
|
+
}
|
|
182
209
|
return ctx.response.redirect(accountHome(cfg));
|
|
183
210
|
}
|
|
184
211
|
return next();
|
|
@@ -770,10 +797,6 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
770
797
|
C.accountOrgs,
|
|
771
798
|
'revokeInvitation',
|
|
772
799
|
]);
|
|
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
800
|
}
|
|
778
801
|
// ─── Account self-service JSON API (authkit-react TanStack hooks) ─────
|
|
779
802
|
// ⚠️ ORDER MATTERS: fixed-segment routes before parameterised ones.
|
|
@@ -925,6 +948,7 @@ export function registerAuthHost(router, opts = {}) {
|
|
|
925
948
|
// Clients OIDC.
|
|
926
949
|
router.get(`${ap}/api/clients`, [C.consoleClients, 'index']);
|
|
927
950
|
router.post(`${ap}/api/clients`, [C.consoleClients, 'store']);
|
|
951
|
+
router.get(`${ap}/api/clients/:id`, [C.consoleClients, 'show']);
|
|
928
952
|
router.patch(`${ap}/api/clients/:id`, [C.consoleClients, 'update']);
|
|
929
953
|
router.delete(`${ap}/api/clients/:id`, [C.consoleClients, 'destroy']);
|
|
930
954
|
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.
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props of every screen authkit-server renders through `cfg.render` — what an
|
|
3
|
+
* `inertiaRenderer` page (or a custom renderer) receives for each view.
|
|
4
|
+
*
|
|
5
|
+
* `AuthkitScreenProps['<view>']` is the shape a page gets, `messages` included
|
|
6
|
+
* (the renderer adds it). The controllers call `cfg.render` with
|
|
7
|
+
* {@link ScreenRenderProps} (the same shape without `messages`), and `render` is
|
|
8
|
+
* typed per view, so a controller that drifts from these types fails the
|
|
9
|
+
* package's `tsc`.
|
|
10
|
+
*/
|
|
11
|
+
import type { OrgInvitation, OrgMember, OrgSummary } from '../accounts/account_store.js';
|
|
12
|
+
import type { PatRecord } from '../pat/pat_store.js';
|
|
13
|
+
import type { AccountConfirmProps, AccountEmailConfirmedProps, AccountLoginProps, AccountMfaProps, AccountSecurityProps } from './account_screen_props.js';
|
|
14
|
+
import type { BotProtectionWidget } from './bot_protection.js';
|
|
15
|
+
import type { ClientBrand } from './branding.js';
|
|
16
|
+
import type { CustomMfaDescriptor } from './custom_mfa.js';
|
|
17
|
+
import type { AuthMessages } from './i18n.js';
|
|
18
|
+
import type { LastLoginMethod } from './last_login_method.js';
|
|
19
|
+
import type { MagicChannelProp } from './login_channel.js';
|
|
20
|
+
import type { ResolvedAuthMethods, ResolvedOrganizationsPolicySetting } from './runtime_toggles.js';
|
|
21
|
+
/** What every screen receives from the renderer. */
|
|
22
|
+
interface WithMessages {
|
|
23
|
+
/** The i18n catalog for the request's locale (added by the renderer). */
|
|
24
|
+
messages: AuthMessages;
|
|
25
|
+
}
|
|
26
|
+
/** The account a login step is about (step 2+), or `null`. */
|
|
27
|
+
export interface LoginAccount {
|
|
28
|
+
fullName: string | null;
|
|
29
|
+
globalRoles: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `login` — the OIDC interaction's login screen. `step` drives the sub-view:
|
|
33
|
+
* `identifier` (ask for the e-mail), `password`, or `password_expired`.
|
|
34
|
+
*/
|
|
35
|
+
export interface LoginProps extends WithMessages {
|
|
36
|
+
uid: string;
|
|
37
|
+
csrfToken: string;
|
|
38
|
+
brand?: ClientBrand;
|
|
39
|
+
step?: 'identifier' | 'password' | 'password_expired';
|
|
40
|
+
email?: string;
|
|
41
|
+
account?: LoginAccount | null;
|
|
42
|
+
error?: string | null;
|
|
43
|
+
/** `true` shows the link to sign up. */
|
|
44
|
+
registrationEnabled?: boolean;
|
|
45
|
+
/** Login methods allowed now (global settings × the account's preference). */
|
|
46
|
+
authMethods?: ResolvedAuthMethods;
|
|
47
|
+
magicLinkAvailable?: boolean;
|
|
48
|
+
otpEnabled?: boolean;
|
|
49
|
+
/** The method this device signed in with last time, for the "last used" badge. */
|
|
50
|
+
lastUsedLoginMethod?: LastLoginMethod;
|
|
51
|
+
passkeyFirstAvailable?: boolean;
|
|
52
|
+
botProtection?: BotProtectionWidget;
|
|
53
|
+
rememberEnabled?: boolean;
|
|
54
|
+
rememberDays?: number;
|
|
55
|
+
/** `true` after a magic link / code was sent. */
|
|
56
|
+
magicLinkSent?: boolean;
|
|
57
|
+
/** Which part of the "sent" state to show; `none` hides both. */
|
|
58
|
+
magicChannel?: MagicChannelProp | 'none';
|
|
59
|
+
/** Error of the one-time code field. */
|
|
60
|
+
otpError?: string;
|
|
61
|
+
}
|
|
62
|
+
/** `signup` — the interaction's registration screen. */
|
|
63
|
+
export interface SignupProps extends WithMessages {
|
|
64
|
+
uid: string;
|
|
65
|
+
csrfToken: string;
|
|
66
|
+
brand?: ClientBrand;
|
|
67
|
+
error?: string;
|
|
68
|
+
registrationDisabled?: boolean;
|
|
69
|
+
registrationDisabledMessage?: string;
|
|
70
|
+
/** `true` when signup sends a magic link instead of asking for a password. */
|
|
71
|
+
passwordlessSignup?: boolean;
|
|
72
|
+
botProtection?: BotProtectionWidget;
|
|
73
|
+
/** `true` after the passwordless signup link was sent. */
|
|
74
|
+
magicLinkSent?: boolean;
|
|
75
|
+
otpEnabled?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** `forgot` — request a password reset e-mail. */
|
|
78
|
+
export interface ForgotProps extends WithMessages {
|
|
79
|
+
csrfToken: string;
|
|
80
|
+
error?: string;
|
|
81
|
+
botProtection?: BotProtectionWidget;
|
|
82
|
+
/** `true` after the e-mail was sent. */
|
|
83
|
+
sent?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/** `reset` — choose a new password from the e-mailed link. */
|
|
86
|
+
export interface ResetProps extends WithMessages {
|
|
87
|
+
token: string;
|
|
88
|
+
csrfToken: string;
|
|
89
|
+
error?: string;
|
|
90
|
+
/** `true` once the password was changed. */
|
|
91
|
+
done?: boolean;
|
|
92
|
+
}
|
|
93
|
+
/** `verify-email` — result of the e-mail verification link. */
|
|
94
|
+
export interface VerifyEmailProps extends WithMessages {
|
|
95
|
+
verified: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** A host-registered second factor, as the `mfa-challenge` screen lists it. */
|
|
98
|
+
export type MfaChallengeCustomMethod = CustomMfaDescriptor & {
|
|
99
|
+
beginUrl: string;
|
|
100
|
+
verifyUrl: string;
|
|
101
|
+
started: boolean;
|
|
102
|
+
};
|
|
103
|
+
/** `mfa-challenge` — the second factor of a login. */
|
|
104
|
+
export interface MfaChallengeProps extends WithMessages {
|
|
105
|
+
uid: string;
|
|
106
|
+
csrfToken: string;
|
|
107
|
+
brand?: ClientBrand;
|
|
108
|
+
error?: string;
|
|
109
|
+
totpAvailable?: boolean;
|
|
110
|
+
passkeyAvailable?: boolean;
|
|
111
|
+
/** MFA is required but the account has no factor enrolled. */
|
|
112
|
+
noEnrollment?: boolean;
|
|
113
|
+
trustedDevicesEnabled?: boolean;
|
|
114
|
+
trustedDeviceDays?: number;
|
|
115
|
+
/** Too many wrong codes: the code field is locked for now. */
|
|
116
|
+
otpLocked?: boolean;
|
|
117
|
+
customMfaMethods?: MfaChallengeCustomMethod[];
|
|
118
|
+
completedMfaMethods?: string[];
|
|
119
|
+
requiredMfaFactors?: number;
|
|
120
|
+
}
|
|
121
|
+
/** A scope on the consent screen. */
|
|
122
|
+
export interface ConsentScope {
|
|
123
|
+
id: string;
|
|
124
|
+
label: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The authorization request behind a consent, as the provider parsed it. Only
|
|
128
|
+
* `client_id` is always there: device-flow and CIBA requests carry no
|
|
129
|
+
* `redirect_uri` / `response_type`, and plain OAuth requests may omit `scope`.
|
|
130
|
+
* Every other request parameter (`audience`, `resource`, `claims`, …) keeps its
|
|
131
|
+
* raw value under its own name.
|
|
132
|
+
*/
|
|
133
|
+
export type ConsentRequestParams = {
|
|
134
|
+
client_id: string;
|
|
135
|
+
redirect_uri?: string;
|
|
136
|
+
response_type?: string;
|
|
137
|
+
scope?: string;
|
|
138
|
+
state?: string;
|
|
139
|
+
prompt?: string;
|
|
140
|
+
nonce?: string;
|
|
141
|
+
code_challenge?: string;
|
|
142
|
+
code_challenge_method?: string;
|
|
143
|
+
} & Record<string, unknown>;
|
|
144
|
+
/** `consent` — the user grants the client the requested scopes. */
|
|
145
|
+
export interface ConsentProps extends WithMessages {
|
|
146
|
+
uid: string;
|
|
147
|
+
csrfToken: string;
|
|
148
|
+
brand?: ClientBrand;
|
|
149
|
+
/** The authorization request parameters. */
|
|
150
|
+
params: ConsentRequestParams;
|
|
151
|
+
/** The client's registered `client_name`, or its `client_id`. */
|
|
152
|
+
clientName: string;
|
|
153
|
+
/** `clientName`, HTML-escaped (Edge raw output). */
|
|
154
|
+
clientNameHtml: string;
|
|
155
|
+
scopes: ConsentScope[];
|
|
156
|
+
}
|
|
157
|
+
/** `maintenance` — maintenance mode is on. */
|
|
158
|
+
export interface MaintenanceProps extends WithMessages {
|
|
159
|
+
uid?: string;
|
|
160
|
+
csrfToken: string;
|
|
161
|
+
brand?: ClientBrand;
|
|
162
|
+
message: string;
|
|
163
|
+
/** Admins may still sign in. */
|
|
164
|
+
adminLoginAllowed?: boolean;
|
|
165
|
+
adminLoginNote?: string;
|
|
166
|
+
}
|
|
167
|
+
/** `otp-unlock` — result of the "unlock my code login" link. */
|
|
168
|
+
export interface OtpUnlockProps extends WithMessages {
|
|
169
|
+
ok: boolean;
|
|
170
|
+
expired?: boolean;
|
|
171
|
+
}
|
|
172
|
+
/** `session-expired` — the interaction expired or was lost. */
|
|
173
|
+
export interface SessionExpiredProps extends WithMessages {
|
|
174
|
+
/** Where the "sign in again" button goes (`interactionRecovery.redirectTo`). */
|
|
175
|
+
loginUrl: string;
|
|
176
|
+
brand?: ClientBrand;
|
|
177
|
+
}
|
|
178
|
+
/** `throttled` — the request hit a rate limit. */
|
|
179
|
+
export interface ThrottledProps extends WithMessages {
|
|
180
|
+
brand?: ClientBrand;
|
|
181
|
+
/** Seconds until a retry is allowed, or `null` when unknown. */
|
|
182
|
+
retryAfter: number | null;
|
|
183
|
+
retryUrl: string;
|
|
184
|
+
}
|
|
185
|
+
/** The agent asking for access, on `agents/consent`. */
|
|
186
|
+
export interface AgentConsentRequest {
|
|
187
|
+
/** Device-flow user code, when the agent uses it. */
|
|
188
|
+
userCode?: string;
|
|
189
|
+
agentName: string | null;
|
|
190
|
+
agentOrigin: string | null;
|
|
191
|
+
logoUri?: string | null;
|
|
192
|
+
scopes: Array<{
|
|
193
|
+
id: string;
|
|
194
|
+
description: string;
|
|
195
|
+
}>;
|
|
196
|
+
/** Extra hidden form fields to send back. */
|
|
197
|
+
hidden?: Array<{
|
|
198
|
+
name: string;
|
|
199
|
+
value: string;
|
|
200
|
+
}>;
|
|
201
|
+
}
|
|
202
|
+
/** `agents/consent` — the user approves a personal agent's request. */
|
|
203
|
+
export interface AgentsConsentProps extends WithMessages {
|
|
204
|
+
csrfToken: string;
|
|
205
|
+
/** The form's POST target. */
|
|
206
|
+
action: string;
|
|
207
|
+
/** `null` while the user still has to enter a user code. */
|
|
208
|
+
request: AgentConsentRequest | null;
|
|
209
|
+
account: {
|
|
210
|
+
email: string | null;
|
|
211
|
+
} | null;
|
|
212
|
+
/** i18n key of the error to show, or `null`. */
|
|
213
|
+
error: string | null;
|
|
214
|
+
}
|
|
215
|
+
/** `agents/done` — outcome of a personal agent consent. */
|
|
216
|
+
export interface AgentsDoneProps extends WithMessages {
|
|
217
|
+
status: 'approved' | 'denied' | 'expired' | 'invalid';
|
|
218
|
+
agentName: string | null;
|
|
219
|
+
scopes: Array<{
|
|
220
|
+
id: string;
|
|
221
|
+
description: string;
|
|
222
|
+
}>;
|
|
223
|
+
}
|
|
224
|
+
/** `account/apps` — apps and personal agents with access to the account. */
|
|
225
|
+
export interface AccountAppsProps extends WithMessages {
|
|
226
|
+
csrfToken: string;
|
|
227
|
+
supported: boolean;
|
|
228
|
+
/** Personal agents with a delegation, or `null` when the feature is off. */
|
|
229
|
+
agents: Array<{
|
|
230
|
+
id: string;
|
|
231
|
+
name: string;
|
|
232
|
+
scopes: Array<{
|
|
233
|
+
id: string;
|
|
234
|
+
description: string;
|
|
235
|
+
}>;
|
|
236
|
+
}> | null;
|
|
237
|
+
/** Flash with the revoked app, or `null`. */
|
|
238
|
+
revoked: string | null;
|
|
239
|
+
apps: Array<{
|
|
240
|
+
clientId: string;
|
|
241
|
+
name: string;
|
|
242
|
+
/** The client's registered `logo_uri`, or `null`. */
|
|
243
|
+
logoUrl: string | null;
|
|
244
|
+
accessTokens: number;
|
|
245
|
+
refreshTokens: number;
|
|
246
|
+
}>;
|
|
247
|
+
}
|
|
248
|
+
/** `account/tokens` — personal access tokens. */
|
|
249
|
+
export interface AccountTokensProps extends WithMessages {
|
|
250
|
+
csrfToken: string;
|
|
251
|
+
/** The token just created, shown once, or `null`. */
|
|
252
|
+
createdToken: string | null;
|
|
253
|
+
tokens: Array<Pick<PatRecord, 'id' | 'name' | 'scopes' | 'audience' | 'lastUsedAt' | 'createdAt'>>;
|
|
254
|
+
}
|
|
255
|
+
/** An organization on `account/orgs`. */
|
|
256
|
+
export type AccountOrg = OrgSummary & {
|
|
257
|
+
role: string;
|
|
258
|
+
members: OrgMember[];
|
|
259
|
+
canManage: boolean;
|
|
260
|
+
isActive: boolean;
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* `account/orgs` — the user's organizations. With `subview: 'accept-invitation'`
|
|
264
|
+
* it is the invitation acceptance screen (`invitation`, `token`).
|
|
265
|
+
*/
|
|
266
|
+
export interface AccountOrgsProps extends WithMessages {
|
|
267
|
+
csrfToken: string;
|
|
268
|
+
supported?: boolean;
|
|
269
|
+
orgs?: AccountOrg[];
|
|
270
|
+
pendingInvitations?: Array<OrgInvitation & {
|
|
271
|
+
orgName: string;
|
|
272
|
+
}>;
|
|
273
|
+
allowSelfCreate?: ResolvedOrganizationsPolicySetting['allowSelfCreate'];
|
|
274
|
+
availableRoles?: ResolvedOrganizationsPolicySetting['roles'];
|
|
275
|
+
subview?: 'accept-invitation';
|
|
276
|
+
invitation?: OrgInvitation | null;
|
|
277
|
+
token?: string;
|
|
278
|
+
}
|
|
279
|
+
/** Props of every screen, by view name. */
|
|
280
|
+
export interface AuthkitScreenProps {
|
|
281
|
+
login: LoginProps;
|
|
282
|
+
signup: SignupProps;
|
|
283
|
+
consent: ConsentProps;
|
|
284
|
+
forgot: ForgotProps;
|
|
285
|
+
reset: ResetProps;
|
|
286
|
+
'verify-email': VerifyEmailProps;
|
|
287
|
+
'mfa-challenge': MfaChallengeProps;
|
|
288
|
+
'otp-unlock': OtpUnlockProps;
|
|
289
|
+
maintenance: MaintenanceProps;
|
|
290
|
+
'session-expired': SessionExpiredProps;
|
|
291
|
+
throttled: ThrottledProps;
|
|
292
|
+
'agents/consent': AgentsConsentProps;
|
|
293
|
+
'agents/done': AgentsDoneProps;
|
|
294
|
+
'account/login': AccountLoginProps;
|
|
295
|
+
'account/tokens': AccountTokensProps;
|
|
296
|
+
'account/mfa': AccountMfaProps;
|
|
297
|
+
'account/security': AccountSecurityProps;
|
|
298
|
+
'account/apps': AccountAppsProps;
|
|
299
|
+
'account/orgs': AccountOrgsProps;
|
|
300
|
+
'account/confirm': AccountConfirmProps;
|
|
301
|
+
'account/email-confirmed': AccountEmailConfirmedProps;
|
|
302
|
+
}
|
|
303
|
+
/** Every screen authkit-server renders. */
|
|
304
|
+
export type AuthkitScreen = keyof AuthkitScreenProps;
|
|
305
|
+
/** What a controller hands `cfg.render` for a view: the page props without `messages`. */
|
|
306
|
+
export type ScreenRenderProps<View extends AuthkitScreen> = Omit<AuthkitScreenProps[View], 'messages'>;
|
|
307
|
+
/** `cfg.render`, typed per view. */
|
|
308
|
+
export type AuthkitScreenRenderer = <View extends AuthkitScreen>(ctx: import('@adonisjs/core/http').HttpContext, view: View, props: ScreenRenderProps<View>) => unknown;
|
|
309
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props of every screen authkit-server renders through `cfg.render` — what an
|
|
3
|
+
* `inertiaRenderer` page (or a custom renderer) receives for each view.
|
|
4
|
+
*
|
|
5
|
+
* `AuthkitScreenProps['<view>']` is the shape a page gets, `messages` included
|
|
6
|
+
* (the renderer adds it). The controllers call `cfg.render` with
|
|
7
|
+
* {@link ScreenRenderProps} (the same shape without `messages`), and `render` is
|
|
8
|
+
* typed per view, so a controller that drifts from these types fails the
|
|
9
|
+
* package's `tsc`.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|