@adonis-agora/authkit-server 0.48.0 → 0.50.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/build/host/views/login.edge +18 -0
- package/build/index.d.ts +18 -2
- package/build/index.js +16 -1
- package/build/src/accounts/account_store.d.ts +59 -1
- package/build/src/accounts/account_store.js +5 -0
- package/build/src/accounts/lucid_store/core.d.ts +2 -2
- package/build/src/accounts/lucid_store/core.js +105 -4
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/define_config.d.ts +22 -0
- package/build/src/define_config.js +5 -0
- package/build/src/host/account_screen_props.d.ts +163 -0
- package/build/src/host/account_screen_props.js +23 -0
- package/build/src/host/controllers/account_confirm_controller.js +3 -2
- package/build/src/host/controllers/account_mfa_controller.js +9 -6
- package/build/src/host/controllers/account_security_controller.js +3 -2
- package/build/src/host/controllers/account_session_controller.js +8 -3
- package/build/src/host/controllers/interaction_controller.d.ts +11 -0
- package/build/src/host/controllers/interaction_controller.js +136 -6
- package/build/src/host/default_mailer.d.ts +1 -0
- package/build/src/host/default_mailer.js +3 -0
- package/build/src/host/email_templates.d.ts +8 -0
- package/build/src/host/email_templates.js +12 -1
- package/build/src/host/i18n.d.ts +14 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/otp_login.d.ts +155 -0
- package/build/src/host/otp_login.js +206 -0
- package/build/src/host/rate_limit.d.ts +6 -0
- package/build/src/host/rate_limit.js +3 -0
- package/build/src/host/register_auth_host.js +8 -0
- package/build/src/host/renderers/inertia_renderer.d.ts +23 -66
- package/build/src/host/renderers/inertia_renderer.js +23 -66
- package/package.json +2 -2
|
@@ -193,6 +193,24 @@
|
|
|
193
193
|
{{-- Passwordless: confirmação de magic link enviado (anti-enumeração). --}}
|
|
194
194
|
@if(magicLinkSent)
|
|
195
195
|
<p class="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">{{ t('login.magic_link_sent') }}</p>
|
|
196
|
+
|
|
197
|
+
{{-- Login por OTP: campo de código digitável (mesmo e-mail carrega link E código). --}}
|
|
198
|
+
@if(otpEnabled)
|
|
199
|
+
<form method="POST" action="/auth/interaction/{{ uid }}/otp-verify" class="mt-4">
|
|
200
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
201
|
+
<label for="otp-code" class="block text-sm font-medium text-gray-700">{{ t('login.otp_label') }}</label>
|
|
202
|
+
<input id="otp-code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code"
|
|
203
|
+
pattern="[0-9]*" placeholder="{{ t('login.otp_placeholder') }}"
|
|
204
|
+
class="mt-2 w-full rounded-lg border border-gray-300 px-3 py-2.5 text-center text-lg tracking-[0.4em] font-mono focus:border-gray-900 focus:ring-gray-900" />
|
|
205
|
+
@if(otpError)
|
|
206
|
+
<p class="mt-2 text-sm text-red-600">{{ otpError }}</p>
|
|
207
|
+
@end
|
|
208
|
+
<button type="submit"
|
|
209
|
+
class="mt-4 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
|
|
210
|
+
{{ t('login.otp_submit') }}
|
|
211
|
+
</button>
|
|
212
|
+
</form>
|
|
213
|
+
@end
|
|
196
214
|
@end
|
|
197
215
|
|
|
198
216
|
{{-- Passwordless: "me envie um link de login" (mesma sessão/e-mail; não pede senha). --}}
|
package/build/index.d.ts
CHANGED
|
@@ -23,8 +23,9 @@ export { lucidAccountStore, appKeyEncrypter } from './src/accounts/lucid_account
|
|
|
23
23
|
export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
24
24
|
export type { LucidStoresModels, LucidStoresOptions, LucidStoresResult, } from './src/accounts/lucid_stores.js';
|
|
25
25
|
export type { LucidAccountStoreOptions, AccountSecretEncrypter, } from './src/accounts/lucid_account_store.js';
|
|
26
|
-
export type { AccountStore, CoreAccountStore, AdminCapability, MfaCapability, WebauthnCapability, ProviderIdentityCapability, ProviderIdentitySummary, AccountSecurityCapability, AccountStatusCapability, ProfileCapability, MagicLinkCapability, EmailVerificationStatusCapability, AccountDeletionCapability, AccountImportCapability, ImportAccountInput, AuthAccount, CreateAccountInput, LinkProviderIdentityInput, ListAccountsParams, Paginated, PasskeySummary, } from './src/accounts/account_store.js';
|
|
27
|
-
export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, supportsAccountStatus, supportsProfile, supportsMagicLink, supportsEmailVerificationStatus, supportsAccountDeletion, supportsAccountImport, } from './src/accounts/account_store.js';
|
|
26
|
+
export type { AccountStore, CoreAccountStore, AdminCapability, MfaCapability, WebauthnCapability, ProviderIdentityCapability, ProviderIdentitySummary, AccountSecurityCapability, AccountStatusCapability, ProfileCapability, MagicLinkCapability, OtpLoginCapability, OtpLoginVerifyResult, EmailVerificationStatusCapability, AccountDeletionCapability, AccountImportCapability, ImportAccountInput, AuthAccount, CreateAccountInput, LinkProviderIdentityInput, ListAccountsParams, Paginated, PasskeySummary, } from './src/accounts/account_store.js';
|
|
27
|
+
export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, supportsAccountStatus, supportsProfile, supportsMagicLink, supportsOtpLogin, supportsEmailVerificationStatus, supportsAccountDeletion, supportsAccountImport, } from './src/accounts/account_store.js';
|
|
28
|
+
export { type OtpLoginConfigInput, type ResolvedOtpLoginConfig, type OtpVerifyOutcome, resolveOtpLoginConfig, generateOtpCode, evaluateLoginOtp, OTP_LOGIN_DEFAULTS, } from './src/host/otp_login.js';
|
|
28
29
|
export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
|
|
29
30
|
export type { PasswordConfigInput, LegacyPasswordVerifier, PasswordVerifyResult, } from './src/password/password_manager.js';
|
|
30
31
|
export { checkPasswordPolicy, policyViolationParams, DEFAULT_PWNED_TIMEOUT_MS, } from './src/password/policy.js';
|
|
@@ -46,6 +47,7 @@ export type { EventsConfigInput, ResolvedEventsConfig } from './src/events/dispa
|
|
|
46
47
|
export { inertiaRenderer } from './src/host/renderers/inertia_renderer.js';
|
|
47
48
|
export type { AuthkitScreen } from './src/host/renderers/inertia_renderer.js';
|
|
48
49
|
export type { InertiaRendererOptions } from './src/host/renderers/inertia_renderer.js';
|
|
50
|
+
export type { AccountLoginProps, AccountSecurityProps, AccountMfaProps, AccountConfirmProps, AccountConfirmMethod, AccountEmailConfirmedProps, } from './src/host/account_screen_props.js';
|
|
49
51
|
export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
|
|
50
52
|
export { brandFor, isFirstParty } from './src/host/branding.js';
|
|
51
53
|
export type { BrandingConfig, ClientBrand } from './src/host/branding.js';
|
|
@@ -55,6 +57,20 @@ export type { AuthHostRenderer, AuthSocialConfig } from './src/define_config.js'
|
|
|
55
57
|
export { registerAuthHost } from './src/host/register_auth_host.js';
|
|
56
58
|
export type { AuthHostOptions } from './src/host/register_auth_host.js';
|
|
57
59
|
export { getAdminPrefix, setAdminPrefix, normalizeAdminPrefix, getAdminApiPrefix, setAdminApiPrefix, normalizeAdminApiPrefix, } from './src/host/admin_prefix.js';
|
|
60
|
+
/**
|
|
61
|
+
* Helpers de path do console de conta (`/account/*`). Um host que precisa casar
|
|
62
|
+
* um middleware ou link com uma rota do console (ex.: `GET
|
|
63
|
+
* {accountPath('security')}/export`) deriva o path daqui em vez de hardcodar,
|
|
64
|
+
* respeitando os overrides de `accountRoutes`.
|
|
65
|
+
*
|
|
66
|
+
* ⚠️ Estes helpers leem um singleton de processo que só reflete os overrides
|
|
67
|
+
* DEPOIS que `registerAuthHost` roda (é ele quem chama `setAccountPaths` com a
|
|
68
|
+
* opção `accountRoutes`, no boot). Chamados antes disso, devolvem os defaults
|
|
69
|
+
* (`/account/*`). Componha URLs em runtime (dentro de handlers/factories de
|
|
70
|
+
* middleware), não em tempo de import de módulo.
|
|
71
|
+
*/
|
|
72
|
+
export { accountPath, joinAccountPath, accountPrefix, } from './src/host/account_paths.js';
|
|
73
|
+
export type { AccountPathsOptions, AccountPathKey } from './src/host/account_paths.js';
|
|
58
74
|
export { resolveRateLimit, resolveNotifications } from './src/define_config.js';
|
|
59
75
|
export type { ResolvedNotificationsConfig } from './src/define_config.js';
|
|
60
76
|
export type { RateLimitConfigInput, RateLimitBucket, ResolvedRateLimitConfig, } from './src/define_config.js';
|
package/build/index.js
CHANGED
|
@@ -14,7 +14,9 @@ export { resolveTrustedDevices, isTrustedDeviceValid, buildTrustedDevicePayload,
|
|
|
14
14
|
export { resolveBotProtection, botProtectionApplies, extractBotToken, verifyBotProtection, guardBotProtection, DEFAULT_BOT_TOKEN_FIELDS, } from './src/host/bot_protection.js';
|
|
15
15
|
export { lucidAccountStore, appKeyEncrypter } from './src/accounts/lucid_account_store.js';
|
|
16
16
|
export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
17
|
-
export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, supportsAccountStatus, supportsProfile, supportsMagicLink, supportsEmailVerificationStatus, supportsAccountDeletion, supportsAccountImport, } from './src/accounts/account_store.js';
|
|
17
|
+
export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, supportsAccountStatus, supportsProfile, supportsMagicLink, supportsOtpLogin, supportsEmailVerificationStatus, supportsAccountDeletion, supportsAccountImport, } from './src/accounts/account_store.js';
|
|
18
|
+
// Login por OTP (código digitável): config + helpers puros.
|
|
19
|
+
export { resolveOtpLoginConfig, generateOtpCode, evaluateLoginOtp, OTP_LOGIN_DEFAULTS, } from './src/host/otp_login.js';
|
|
18
20
|
// Gerência de senha: lazy rehash + legacy verifier, política e checagem de vazamento.
|
|
19
21
|
export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
|
|
20
22
|
export { checkPasswordPolicy, policyViolationParams, DEFAULT_PWNED_TIMEOUT_MS, } from './src/password/policy.js';
|
|
@@ -32,6 +34,19 @@ export { brandFor, isFirstParty } from './src/host/branding.js';
|
|
|
32
34
|
export { resolveMessages, translate, DEFAULT_MESSAGES, PT_BR_MESSAGES, BUILTIN_MESSAGES, DEFAULT_LOCALE, } from './src/host/i18n.js';
|
|
33
35
|
export { registerAuthHost } from './src/host/register_auth_host.js';
|
|
34
36
|
export { getAdminPrefix, setAdminPrefix, normalizeAdminPrefix, getAdminApiPrefix, setAdminApiPrefix, normalizeAdminApiPrefix, } from './src/host/admin_prefix.js';
|
|
37
|
+
/**
|
|
38
|
+
* Helpers de path do console de conta (`/account/*`). Um host que precisa casar
|
|
39
|
+
* um middleware ou link com uma rota do console (ex.: `GET
|
|
40
|
+
* {accountPath('security')}/export`) deriva o path daqui em vez de hardcodar,
|
|
41
|
+
* respeitando os overrides de `accountRoutes`.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ Estes helpers leem um singleton de processo que só reflete os overrides
|
|
44
|
+
* DEPOIS que `registerAuthHost` roda (é ele quem chama `setAccountPaths` com a
|
|
45
|
+
* opção `accountRoutes`, no boot). Chamados antes disso, devolvem os defaults
|
|
46
|
+
* (`/account/*`). Componha URLs em runtime (dentro de handlers/factories de
|
|
47
|
+
* middleware), não em tempo de import de módulo.
|
|
48
|
+
*/
|
|
49
|
+
export { accountPath, joinAccountPath, accountPrefix, } from './src/host/account_paths.js';
|
|
35
50
|
export { resolveRateLimit, resolveNotifications } from './src/define_config.js';
|
|
36
51
|
export { createAuthThrottles } from './src/host/rate_limit.js';
|
|
37
52
|
/**
|
|
@@ -421,6 +421,62 @@ export interface MagicLinkCapability {
|
|
|
421
421
|
*/
|
|
422
422
|
consumeMagicLinkToken(token: string): Promise<AuthAccount | null>;
|
|
423
423
|
}
|
|
424
|
+
/** Resultado tipado da verificação de um código OTP de login. */
|
|
425
|
+
export type OtpLoginVerifyResult = {
|
|
426
|
+
status: 'ok';
|
|
427
|
+
account: AuthAccount;
|
|
428
|
+
}
|
|
429
|
+
/** Código errado, tentativa contabilizada (ainda NÃO travado). */
|
|
430
|
+
| {
|
|
431
|
+
status: 'invalid';
|
|
432
|
+
}
|
|
433
|
+
/** Tentativas esgotadas → código invalidado (o link continua válido). */
|
|
434
|
+
| {
|
|
435
|
+
status: 'locked';
|
|
436
|
+
}
|
|
437
|
+
/** TTL do código expirou. */
|
|
438
|
+
| {
|
|
439
|
+
status: 'expired';
|
|
440
|
+
}
|
|
441
|
+
/** Nenhum código pendente para esta interaction/conta. */
|
|
442
|
+
| {
|
|
443
|
+
status: 'no_code';
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Login por OTP (código digitável) — extensão do magic link. CAPACIDADE
|
|
447
|
+
* opcional: quando ausente (ou `login.otp.enabled` desligado) o comportamento é
|
|
448
|
+
* exatamente o de antes (só magic link).
|
|
449
|
+
*
|
|
450
|
+
* O store default (Lucid) CO-LOCALIZA o código com o magic link no MESMO slot
|
|
451
|
+
* (`passwordResetToken`, prefixo `ml2:`), de modo que consumir um mata o outro
|
|
452
|
+
* (single-use conjunto) e o contador de tentativas fica PERSISTIDO junto do
|
|
453
|
+
* código (lockout fail-closed, sem depender de limiter). Ver `host/otp_login.ts`
|
|
454
|
+
* para a decisão de armazenamento completa.
|
|
455
|
+
*/
|
|
456
|
+
export interface OtpLoginCapability {
|
|
457
|
+
/**
|
|
458
|
+
* Emite o magic link E um código OTP de uma vez (mesmo disparo/e-mail). O
|
|
459
|
+
* `token` retornado vai na URL do link; o `code` (dígitos) vai no corpo do
|
|
460
|
+
* e-mail. Retorna null se a conta não existe (o controller sempre responde
|
|
461
|
+
* "enviado", anti-enumeração). O código fica atrelado ao `uid` da interaction.
|
|
462
|
+
*/
|
|
463
|
+
issueMagicLinkWithCode(email: string, uid: string, opts: {
|
|
464
|
+
digits: number;
|
|
465
|
+
ttlMinutes: number;
|
|
466
|
+
}): Promise<{
|
|
467
|
+
token: string;
|
|
468
|
+
code: string;
|
|
469
|
+
account: AuthAccount;
|
|
470
|
+
} | null>;
|
|
471
|
+
/**
|
|
472
|
+
* Verifica um código para a interaction `uid`. Em sucesso consome o código E o
|
|
473
|
+
* magic link (single-use conjunto). Falha incrementa o contador persistido; ao
|
|
474
|
+
* esgotar `maxAttempts` invalida o código mantendo o link válido.
|
|
475
|
+
*/
|
|
476
|
+
verifyLoginCode(email: string, uid: string, code: string, opts: {
|
|
477
|
+
maxAttempts: number;
|
|
478
|
+
}): Promise<OtpLoginVerifyResult>;
|
|
479
|
+
}
|
|
424
480
|
/** DTO público de uma organização. */
|
|
425
481
|
export interface OrgSummary {
|
|
426
482
|
id: string;
|
|
@@ -543,7 +599,7 @@ export type AccountStore = CoreAccountStore & {
|
|
|
543
599
|
* blocos `Partial<...>` de capacidades probáveis.
|
|
544
600
|
*/
|
|
545
601
|
readonly connectionName?: string;
|
|
546
|
-
} & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability & AccountStatusCapability & ProfileCapability & MagicLinkCapability & EmailVerificationStatusCapability & AccountDeletionCapability & AccountImportCapability & OrganizationsCapability & PasswordHistoryCapability & PasswordExpirationCapability>;
|
|
602
|
+
} & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability & AccountStatusCapability & ProfileCapability & MagicLinkCapability & OtpLoginCapability & EmailVerificationStatusCapability & AccountDeletionCapability & AccountImportCapability & OrganizationsCapability & PasswordHistoryCapability & PasswordExpirationCapability>;
|
|
547
603
|
/** Type guard: o store implementa a capacidade de MFA / TOTP. */
|
|
548
604
|
export declare function supportsMfa(store: AccountStore): store is AccountStore & MfaCapability;
|
|
549
605
|
/**
|
|
@@ -566,6 +622,8 @@ export declare function supportsAccountStatus(store: AccountStore): store is Acc
|
|
|
566
622
|
export declare function supportsProfile(store: AccountStore): store is AccountStore & ProfileCapability;
|
|
567
623
|
/** Type guard: o store implementa login por magic link (passwordless). */
|
|
568
624
|
export declare function supportsMagicLink(store: AccountStore): store is AccountStore & MagicLinkCapability;
|
|
625
|
+
/** Type guard: o store implementa o login por OTP (código digitável). */
|
|
626
|
+
export declare function supportsOtpLogin(store: AccountStore): store is AccountStore & OtpLoginCapability;
|
|
569
627
|
/** Type guard: o store consegue dizer se o e-mail de uma conta está verificado. */
|
|
570
628
|
export declare function supportsEmailVerificationStatus(store: AccountStore): store is AccountStore & EmailVerificationStatusCapability;
|
|
571
629
|
/** Type guard: o store implementa a deleção (hard delete) da conta. */
|
|
@@ -38,6 +38,11 @@ export function supportsProfile(store) {
|
|
|
38
38
|
export function supportsMagicLink(store) {
|
|
39
39
|
return typeof store.issueMagicLinkToken === 'function';
|
|
40
40
|
}
|
|
41
|
+
/** Type guard: o store implementa o login por OTP (código digitável). */
|
|
42
|
+
export function supportsOtpLogin(store) {
|
|
43
|
+
return (typeof store.issueMagicLinkWithCode === 'function' &&
|
|
44
|
+
typeof store.verifyLoginCode === 'function');
|
|
45
|
+
}
|
|
41
46
|
/** Type guard: o store consegue dizer se o e-mail de uma conta está verificado. */
|
|
42
47
|
export function supportsEmailVerificationStatus(store) {
|
|
43
48
|
return typeof store.isEmailVerified === 'function';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccountImportCapability, AccountSecurityCapability, CoreAccountStore, MagicLinkCapability } from '../account_store.js';
|
|
1
|
+
import type { AccountImportCapability, AccountSecurityCapability, CoreAccountStore, MagicLinkCapability, OtpLoginCapability } from '../account_store.js';
|
|
2
2
|
import type { LucidStoreContext } from './shared.js';
|
|
3
3
|
/**
|
|
4
4
|
* Núcleo SEMPRE presente do {@link CoreAccountStore} sobre um model Lucid:
|
|
@@ -6,4 +6,4 @@ import type { LucidStoreContext } from './shared.js';
|
|
|
6
6
|
* (listagem paginada + roles globais) e o self-service de segurança
|
|
7
7
|
* ({@link AccountSecurityCapability}: trocar senha/e-mail).
|
|
8
8
|
*/
|
|
9
|
-
export declare function buildCore(ctx: LucidStoreContext): CoreAccountStore & AccountSecurityCapability & MagicLinkCapability & AccountImportCapability;
|
|
9
|
+
export declare function buildCore(ctx: LucidStoreContext): CoreAccountStore & AccountSecurityCapability & MagicLinkCapability & OtpLoginCapability & AccountImportCapability;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt';
|
|
3
3
|
import { DateTime } from 'luxon';
|
|
4
|
+
import { OTP_LOGIN_PREFIX, decodeOtpToken, encodeOtpToken, evaluateLoginOtp, generateOtpCode, hashLoginOtp, linkTokenFromOtpUrl, } from '../../host/otp_login.js';
|
|
4
5
|
import { hasColumn } from './status_profile.js';
|
|
5
6
|
/** Prefixo do token de troca de e-mail (reaproveita a coluna emailVerificationToken). */
|
|
6
7
|
const EMAIL_CHANGE_PREFIX = 'ec:';
|
|
@@ -131,9 +132,9 @@ export function buildCore(ctx) {
|
|
|
131
132
|
return { token, account: toAccount(row) };
|
|
132
133
|
},
|
|
133
134
|
async consumePasswordResetToken(token, newPassword) {
|
|
134
|
-
// Magic links (`ml:`) NÃO são tokens de reset de senha —
|
|
135
|
-
// consumeMagicLinkToken pode consumi-los (não trocam senha).
|
|
136
|
-
if (token.startsWith(MAGIC_LINK_PREFIX))
|
|
135
|
+
// Magic links (`ml:` e `ml2:` com OTP) NÃO são tokens de reset de senha —
|
|
136
|
+
// só o fluxo de consumeMagicLinkToken pode consumi-los (não trocam senha).
|
|
137
|
+
if (token.startsWith(MAGIC_LINK_PREFIX) || token.startsWith(OTP_LOGIN_PREFIX))
|
|
137
138
|
return false;
|
|
138
139
|
const row = await Model.query().where('passwordResetToken', token).first();
|
|
139
140
|
if (!row)
|
|
@@ -167,7 +168,29 @@ export function buildCore(ctx) {
|
|
|
167
168
|
return { token, account: toAccount(row) };
|
|
168
169
|
},
|
|
169
170
|
async consumeMagicLinkToken(token) {
|
|
170
|
-
if (!token
|
|
171
|
+
if (!token)
|
|
172
|
+
return null;
|
|
173
|
+
// Magic link com OTP ativo: o slot guarda `ml2:<linkToken>:<...>` mas a URL
|
|
174
|
+
// carrega só `ml2:<linkToken>`. Busca pelo prefixo do link (linkToken é hex
|
|
175
|
+
// validado — sem metacaractere de LIKE) e consome o slot inteiro (mata o
|
|
176
|
+
// código junto — single-use conjunto).
|
|
177
|
+
if (token.startsWith(OTP_LOGIN_PREFIX)) {
|
|
178
|
+
const linkToken = linkTokenFromOtpUrl(token);
|
|
179
|
+
if (!linkToken)
|
|
180
|
+
return null;
|
|
181
|
+
const row = await Model.query()
|
|
182
|
+
.where('passwordResetToken', 'like', `${OTP_LOGIN_PREFIX}${linkToken}:%`)
|
|
183
|
+
.first();
|
|
184
|
+
if (!row)
|
|
185
|
+
return null;
|
|
186
|
+
if (!row.passwordResetExpiresAt || row.passwordResetExpiresAt < DateTime.now())
|
|
187
|
+
return null;
|
|
188
|
+
row.passwordResetToken = null;
|
|
189
|
+
row.passwordResetExpiresAt = null;
|
|
190
|
+
await row.save();
|
|
191
|
+
return toAccount(row);
|
|
192
|
+
}
|
|
193
|
+
if (!token.startsWith(MAGIC_LINK_PREFIX))
|
|
171
194
|
return null;
|
|
172
195
|
const row = await Model.query().where('passwordResetToken', token).first();
|
|
173
196
|
if (!row)
|
|
@@ -180,6 +203,84 @@ export function buildCore(ctx) {
|
|
|
180
203
|
await row.save();
|
|
181
204
|
return toAccount(row);
|
|
182
205
|
},
|
|
206
|
+
// ----- Login por OTP (código digitável — extensão do magic link) -----
|
|
207
|
+
async issueMagicLinkWithCode(email, uid, opts) {
|
|
208
|
+
const row = await Model.query().where('email', email).first();
|
|
209
|
+
if (!row)
|
|
210
|
+
return null;
|
|
211
|
+
const linkToken = randomBytes(32).toString('hex');
|
|
212
|
+
const code = generateOtpCode(opts.digits);
|
|
213
|
+
const codeHash = hashLoginOtp(uid, code);
|
|
214
|
+
const codeExpMs = DateTime.now().plus({ minutes: opts.ttlMinutes }).toMillis();
|
|
215
|
+
// Slot `ml2:` — código + link juntos, contador em 0. Ver host/otp_login.ts.
|
|
216
|
+
row.passwordResetToken = encodeOtpToken({ linkToken, codeHash, codeExpMs, attempts: 0 });
|
|
217
|
+
// O LINK herda a validade padrão do magic link (15 min); o CÓDIGO carrega o
|
|
218
|
+
// próprio `codeExpMs` (mais curto) embutido no slot.
|
|
219
|
+
row.passwordResetExpiresAt = DateTime.now().plus({ minutes: 15 });
|
|
220
|
+
await row.save();
|
|
221
|
+
return { token: `${OTP_LOGIN_PREFIX}${linkToken}`, code, account: toAccount(row) };
|
|
222
|
+
},
|
|
223
|
+
async verifyLoginCode(email, uid, code, opts) {
|
|
224
|
+
// ── Atomicidade do contador de lockout (barreira PRIMÁRIA, fail-closed) ──
|
|
225
|
+
// O contador de tentativas vive DENTRO do slot `ml2:` e é a única barreira
|
|
226
|
+
// contra brute-force do código curto (o throttle de rota é camada EXTRA e
|
|
227
|
+
// pode estar ausente). Um read-modify-write ingênuo (first→avaliar→save) é
|
|
228
|
+
// derrotável por concorrência: N requests leem o MESMO contador, todos
|
|
229
|
+
// gravam `attempts+1` (last-write-wins) e o lockout nunca dispara — pior,
|
|
230
|
+
// como a COMPARAÇÃO do código acontece após a leitura, N requests
|
|
231
|
+
// concorrentes conseguem N comparações contra o MESMO valor do contador,
|
|
232
|
+
// varrendo o espaço de 10^6 dentro do TTL.
|
|
233
|
+
//
|
|
234
|
+
// Correção: serializa o read-compare-write numa TRANSAÇÃO com row-lock
|
|
235
|
+
// (`forUpdate`). Cada tentativa lê o estado JÁ commitado pela anterior, o
|
|
236
|
+
// contador avança 1-a-1 e — porque a comparação vive DENTRO da seção
|
|
237
|
+
// crítica — o total de comparações contra um mesmo código fica limitado a
|
|
238
|
+
// `maxAttempts` (garantia DURA, não probabilística). No Postgres o lock é
|
|
239
|
+
// por linha; no sqlite a própria transação serializa. Os demais caminhos
|
|
240
|
+
// que tocam o slot (`consumeMagicLinkToken`, sucesso do OTP) só gravam
|
|
241
|
+
// `null` (terminal) — não regridem contador — e ainda serializam atrás
|
|
242
|
+
// deste lock (todo UPDATE trava a linha), então não podem ressuscitar um
|
|
243
|
+
// slot já consumido nem apagar um incremento.
|
|
244
|
+
const trx = await Model.query().client.transaction();
|
|
245
|
+
try {
|
|
246
|
+
const row = await Model.query({ client: trx }).where('email', email).forUpdate().first();
|
|
247
|
+
if (!row) {
|
|
248
|
+
await trx.commit();
|
|
249
|
+
return { status: 'no_code' };
|
|
250
|
+
}
|
|
251
|
+
const parsed = decodeOtpToken(row.passwordResetToken);
|
|
252
|
+
const evaluation = evaluateLoginOtp({
|
|
253
|
+
parsed,
|
|
254
|
+
uid,
|
|
255
|
+
code,
|
|
256
|
+
nowMs: DateTime.now().toMillis(),
|
|
257
|
+
maxAttempts: opts.maxAttempts,
|
|
258
|
+
});
|
|
259
|
+
// Efeito de persistência: `undefined` = não escreve; `null` = limpa o slot
|
|
260
|
+
// (sucesso, mata o link junto); string = novo slot (contador++/invalidação).
|
|
261
|
+
// A escrita ocorre DENTRO da mesma transação/lock da leitura.
|
|
262
|
+
if (evaluation.nextToken === null) {
|
|
263
|
+
row.useTransaction(trx);
|
|
264
|
+
row.passwordResetToken = null;
|
|
265
|
+
row.passwordResetExpiresAt = null;
|
|
266
|
+
await row.save();
|
|
267
|
+
}
|
|
268
|
+
else if (typeof evaluation.nextToken === 'string') {
|
|
269
|
+
// Contador/invalidação: preserva a validade do LINK (só o código muda).
|
|
270
|
+
row.useTransaction(trx);
|
|
271
|
+
row.passwordResetToken = evaluation.nextToken;
|
|
272
|
+
await row.save();
|
|
273
|
+
}
|
|
274
|
+
await trx.commit();
|
|
275
|
+
if (evaluation.result === 'ok')
|
|
276
|
+
return { status: 'ok', account: toAccount(row) };
|
|
277
|
+
return { status: evaluation.result };
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
await trx.rollback();
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
183
284
|
async issueEmailVerificationToken(email) {
|
|
184
285
|
const row = await Model.query().where('email', email).first();
|
|
185
286
|
if (!row)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP.
|
|
3
3
|
*/
|
|
4
|
-
export type AuditEventType = 'login.success' | 'login.failure' | 'signup' | 'password_reset.issued' | 'password_reset.consumed' | 'pat.issued' | 'pat.revoked' | 'pat.used' | 'impersonation' | 'impersonation.started' | 'mfa.enabled' | 'mfa.disabled' | 'account.locked' | 'passkey.registered' | 'passkey.removed' | 'email_verification.issued' | 'email_verification.consumed' | 'client.created' | 'client.updated' | 'client.deleted' | 'session.revoked_all' | 'password.changed' | 'password.rehashed' | 'email.change_requested' | 'email.changed' | 'login.new_ip_notified' | 'login.new_device' | 'bot_protection.rejected' | 'grant.revoked_by_user' | 'user.created' | 'user.password_reset_sent' | 'user.disabled' | 'user.enabled' | 'user.deleted' | 'profile.updated' | 'account.deleted' | 'account.exported' | 'keys.rotated' | 'organization.created' | 'organization.updated' | 'organization.deleted' | 'organization.member_added' | 'organization.member_removed' | 'organization.member_role_changed' | 'organization.member_role_updated' | 'organization.switched' | 'organization.deactivated' | 'organization.invitation_sent' | 'organization.invitation_accepted' | 'organization.invitation_revoked' | 'email_change.requested' | 'email_change.confirmed' | 'email_change.cancelled' | 'security_notice.sent' | 'settings.updated' | 'maintenance.enabled' | 'maintenance.disabled' | 'trusted_device.revoked' | 'password.expired_change_forced' | 'otp.locked' | 'otp.unlocked' | 'otp.unlock_failed' | 'sudo.confirmed' | 'session.single_enforced' | 'account.expired_login_blocked' | 'account.expiration_warned';
|
|
4
|
+
export type AuditEventType = 'login.success' | 'login.failure' | 'signup' | 'password_reset.issued' | 'password_reset.consumed' | 'pat.issued' | 'pat.revoked' | 'pat.used' | 'impersonation' | 'impersonation.started' | 'mfa.enabled' | 'mfa.disabled' | 'account.locked' | 'passkey.registered' | 'passkey.removed' | 'email_verification.issued' | 'email_verification.consumed' | 'client.created' | 'client.updated' | 'client.deleted' | 'session.revoked_all' | 'password.changed' | 'password.rehashed' | 'email.change_requested' | 'email.changed' | 'login.new_ip_notified' | 'login.new_device' | 'login.otp_sent' | 'login.otp_verified' | 'login.otp_failed' | 'login.otp_invalidated' | 'bot_protection.rejected' | 'grant.revoked_by_user' | 'user.created' | 'user.password_reset_sent' | 'user.disabled' | 'user.enabled' | 'user.deleted' | 'profile.updated' | 'account.deleted' | 'account.exported' | 'keys.rotated' | 'organization.created' | 'organization.updated' | 'organization.deleted' | 'organization.member_added' | 'organization.member_removed' | 'organization.member_role_changed' | 'organization.member_role_updated' | 'organization.switched' | 'organization.deactivated' | 'organization.invitation_sent' | 'organization.invitation_accepted' | 'organization.invitation_revoked' | 'email_change.requested' | 'email_change.confirmed' | 'email_change.cancelled' | 'security_notice.sent' | 'settings.updated' | 'maintenance.enabled' | 'maintenance.disabled' | 'trusted_device.revoked' | 'password.expired_change_forced' | 'otp.locked' | 'otp.unlocked' | 'otp.unlock_failed' | 'sudo.confirmed' | 'session.single_enforced' | 'account.expired_login_blocked' | 'account.expiration_warned';
|
|
5
5
|
/**
|
|
6
6
|
* Evento de auditoria a registrar. O timestamp é definido pelo sink (não aqui).
|
|
7
7
|
*/
|
|
@@ -8,6 +8,7 @@ import { type BotProtectionConfigInput, type ResolvedBotProtectionConfig } from
|
|
|
8
8
|
import type { BrandingConfig } from './host/branding.js';
|
|
9
9
|
import type { ResolveGeo } from './host/geo.js';
|
|
10
10
|
import { type AuthMessages, type I18nConfig } from './host/i18n.js';
|
|
11
|
+
import { type OtpLoginConfigInput, type ResolvedOtpLoginConfig } from './host/otp_login.js';
|
|
11
12
|
import type { SudoMethod } from './host/sudo/types.js';
|
|
12
13
|
import { type ResolvedTrustedDevicesConfig, type TrustedDevicesConfigInput } from './host/trusted_device.js';
|
|
13
14
|
import type { PatStore } from './pat/pat_store.js';
|
|
@@ -40,6 +41,12 @@ export interface MailHooks {
|
|
|
40
41
|
email: string;
|
|
41
42
|
magicUrl: string;
|
|
42
43
|
token: string;
|
|
44
|
+
/**
|
|
45
|
+
* Código OTP de login, presente APENAS quando `login.otp.enabled` está
|
|
46
|
+
* ligado. O host pode montar o próprio e-mail com link E código. Ausente no
|
|
47
|
+
* fluxo só-magic-link (back-compat).
|
|
48
|
+
*/
|
|
49
|
+
code?: string;
|
|
43
50
|
}) => Promise<void>;
|
|
44
51
|
/**
|
|
45
52
|
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Distinto de
|
|
@@ -191,6 +198,13 @@ export interface ResolvedRateLimitConfig {
|
|
|
191
198
|
* afrouxar — o ponto é separar a CONTAGEM, não o teto.
|
|
192
199
|
*/
|
|
193
200
|
sudo: RateLimitBucket;
|
|
201
|
+
/**
|
|
202
|
+
* Bucket da verificação de código OTP de login (`/auth/interaction/:uid/otp-verify`),
|
|
203
|
+
* keyed por IP. MAIS APERTADO que o login (5/min vs 10/min): um código de 6
|
|
204
|
+
* dígitos é adivinhável, então o teto por IP é a primeira barreira anti-brute
|
|
205
|
+
* force ANTES do lockout por interaction (contador persistido no slot do código).
|
|
206
|
+
*/
|
|
207
|
+
otpLogin: RateLimitBucket;
|
|
194
208
|
store?: string;
|
|
195
209
|
}
|
|
196
210
|
export declare function resolveRateLimit(input?: RateLimitConfigInput): ResolvedRateLimitConfig;
|
|
@@ -440,9 +454,17 @@ export declare function resolveAuthMethodsConfig(input?: AuthMethodsConfigInput)
|
|
|
440
454
|
export interface LoginConfigInput {
|
|
441
455
|
/** Exige e-mail verificado para autenticar (senha/magic link/passkey-first). Default: false. */
|
|
442
456
|
requireVerifiedEmail?: boolean;
|
|
457
|
+
/**
|
|
458
|
+
* Login por OTP (código digitável) — extensão do magic link. Quando ligado, o
|
|
459
|
+
* MESMO e-mail passa a carregar link E código, os dois completando a mesma
|
|
460
|
+
* interaction. Default: **desligado** (opt-in; sem a config o comportamento é
|
|
461
|
+
* idêntico ao de antes, e-mail idêntico). Ver `host/otp_login.ts`.
|
|
462
|
+
*/
|
|
463
|
+
otp?: OtpLoginConfigInput;
|
|
443
464
|
}
|
|
444
465
|
export interface ResolvedLoginConfig {
|
|
445
466
|
requireVerifiedEmail: boolean;
|
|
467
|
+
otp: ResolvedOtpLoginConfig;
|
|
446
468
|
}
|
|
447
469
|
export declare function resolveLogin(input?: LoginConfigInput): ResolvedLoginConfig;
|
|
448
470
|
/**
|
|
@@ -4,6 +4,7 @@ import { composeAuditSink, resolveEvents } from './events/dispatcher.js';
|
|
|
4
4
|
import { resolveBotProtection, } from './host/bot_protection.js';
|
|
5
5
|
import { deriveLockedSettingKeys } from './host/config_locks.js';
|
|
6
6
|
import { resolveMessages } from './host/i18n.js';
|
|
7
|
+
import { resolveOtpLoginConfig, } from './host/otp_login.js';
|
|
7
8
|
import { edgeRenderer } from './host/renderers/edge_renderer.js';
|
|
8
9
|
import { resolveTrustedDevices, } from './host/trusted_device.js';
|
|
9
10
|
import { generateJwks } from './keys/jwks_manager.js';
|
|
@@ -17,6 +18,8 @@ const RATE_LIMIT_DEFAULTS = {
|
|
|
17
18
|
adminIp: { points: 30, duration: '1 min' },
|
|
18
19
|
// Mesmos limites do login, bucket separado — ver `ResolvedRateLimitConfig.sudo`.
|
|
19
20
|
sudo: { points: 10, duration: '1 min' },
|
|
21
|
+
// Mais apertado que o login — verificação de código adivinhável.
|
|
22
|
+
otpLogin: { points: 5, duration: '1 min' },
|
|
20
23
|
};
|
|
21
24
|
export function resolveRateLimit(input) {
|
|
22
25
|
const enabled = input?.enabled ?? true;
|
|
@@ -26,6 +29,7 @@ export function resolveRateLimit(input) {
|
|
|
26
29
|
introspection: RATE_LIMIT_DEFAULTS.introspection,
|
|
27
30
|
adminIp: RATE_LIMIT_DEFAULTS.adminIp,
|
|
28
31
|
sudo: RATE_LIMIT_DEFAULTS.sudo,
|
|
32
|
+
otpLogin: RATE_LIMIT_DEFAULTS.otpLogin,
|
|
29
33
|
store: input?.store,
|
|
30
34
|
};
|
|
31
35
|
}
|
|
@@ -119,6 +123,7 @@ export function resolveAuthMethodsConfig(input) {
|
|
|
119
123
|
export function resolveLogin(input) {
|
|
120
124
|
return {
|
|
121
125
|
requireVerifiedEmail: input?.requireVerifiedEmail ?? false,
|
|
126
|
+
otp: resolveOtpLoginConfig(input?.otp),
|
|
122
127
|
};
|
|
123
128
|
}
|
|
124
129
|
export function resolveRegistration(input) {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos de props das telas do console de conta (`/account/*`) — a FONTE ÚNICA da
|
|
3
|
+
* verdade sobre o shape que cada página React recebe.
|
|
4
|
+
*
|
|
5
|
+
* ── Por que esses tipos existem ──────────────────────────────────────────────
|
|
6
|
+
* Um host que cria as telas do console em React próprio (via `inertiaRenderer`,
|
|
7
|
+
* em vez das views Edge built-in) precisa tipar o componente da página. Antes
|
|
8
|
+
* disso, o único "contrato" era o DOCBLOCK do `inertiaRenderer`, copiado à mão —
|
|
9
|
+
* frágil: um docblock desatualizado já enganou um host. Agora o shape vem daqui,
|
|
10
|
+
* e os CONTROLLERS REAIS (`account_session_controller`, `account_security_-
|
|
11
|
+
* controller`, `account_mfa_controller`, `account_confirm_controller`) satisfazem
|
|
12
|
+
* (`satisfies Omit<…, 'messages'>`) exatamente estes tipos ao chamar `render()`.
|
|
13
|
+
* Se o payload de um controller divergir do tipo — campo a mais, a menos, tipo
|
|
14
|
+
* trocado — o `tsc` do pacote quebra. Esse é o objetivo: um único ponto muda, e
|
|
15
|
+
* o compilador força o outro a acompanhar.
|
|
16
|
+
*
|
|
17
|
+
* ── Sobre a prop `messages` ──────────────────────────────────────────────────
|
|
18
|
+
* `messages` (catálogo i18n) é injetada pelo `inertiaRenderer` como shared prop,
|
|
19
|
+
* NÃO pelos controllers. Por isso ela faz parte destes tipos (é o que a página
|
|
20
|
+
* React recebe), mas os controllers satisfazem `Omit<…, 'messages'>` — eles nunca
|
|
21
|
+
* passam `messages` no literal do `render()`.
|
|
22
|
+
*/
|
|
23
|
+
import type { PasskeySummary } from '../accounts/account_store.js';
|
|
24
|
+
import type { AuthMessages } from './i18n.js';
|
|
25
|
+
import type { SudoMethodDescriptor } from './sudo/types.js';
|
|
26
|
+
/**
|
|
27
|
+
* Props da tela `account/login` (tela de login do console de conta).
|
|
28
|
+
*
|
|
29
|
+
* - `csrfToken`: token para o campo `_csrf` do formulário.
|
|
30
|
+
* - `returnTo`: caminho interno de destino pós-login (já validado pelo servidor —
|
|
31
|
+
* só caminhos internos) ou `null`. Quando presente, o formulário deve incluir
|
|
32
|
+
* `<input type="hidden" name="return_to" value={returnTo} />`; o servidor
|
|
33
|
+
* revalida no POST.
|
|
34
|
+
* - `error`: mensagem de erro de autenticação localizada (credenciais inválidas,
|
|
35
|
+
* conta bloqueada/desabilitada). Ausente quando não há erro.
|
|
36
|
+
* - `messages`: catálogo i18n (injetado pelo renderer).
|
|
37
|
+
*/
|
|
38
|
+
export interface AccountLoginProps {
|
|
39
|
+
csrfToken: string;
|
|
40
|
+
returnTo: string | null;
|
|
41
|
+
error?: string;
|
|
42
|
+
messages: AuthMessages;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Props da tela `account/security` (perfil, senha, e-mail, sessões, export,
|
|
46
|
+
* danger-zone). Todas as flags de `*Supported` degradam a UI quando o store não
|
|
47
|
+
* suporta a capacidade correspondente.
|
|
48
|
+
*
|
|
49
|
+
* - `supported`: `false` quando o store não suporta o self-service de segurança.
|
|
50
|
+
* - `profileSupported`: `true` quando dá para editar nome/avatar (`updateProfile`).
|
|
51
|
+
* - `avatarUploadSupported`: `true` quando algum backend (drive OU media) armazena o upload.
|
|
52
|
+
* - `email` / `name` / `avatarUrl`: valores atuais da conta (`''` se ausentes).
|
|
53
|
+
* - `passwordChanged` / `emailChangeRequested` / `emailChanged` / `profileUpdated`
|
|
54
|
+
* / `error` / `trustedDevicesRevoked` / `deleteError`: flashes localizados ou `null`.
|
|
55
|
+
* - `trustedDevicesEnabled`: recurso de dispositivos confiáveis ligado.
|
|
56
|
+
* - `sessionsSupported`: `true` quando o adapter OIDC enumera as sessões ativas.
|
|
57
|
+
* - `sessions`: sessões ativas da própria conta (vazio quando não suportado).
|
|
58
|
+
* `loginTs` é ISO ou `''`.
|
|
59
|
+
* - `exportSupported`: sempre `true` (portabilidade/LGPD para a conta logada).
|
|
60
|
+
* - `deletionSupported`: `true` quando o store suporta hard delete.
|
|
61
|
+
* - `messages`: catálogo i18n (injetado pelo renderer).
|
|
62
|
+
*/
|
|
63
|
+
export interface AccountSecurityProps {
|
|
64
|
+
csrfToken: string;
|
|
65
|
+
supported: boolean;
|
|
66
|
+
profileSupported: boolean;
|
|
67
|
+
avatarUploadSupported: boolean;
|
|
68
|
+
email: string;
|
|
69
|
+
name: string;
|
|
70
|
+
avatarUrl: string;
|
|
71
|
+
passwordChanged: string | null;
|
|
72
|
+
emailChangeRequested: string | null;
|
|
73
|
+
emailChanged: string | null;
|
|
74
|
+
profileUpdated: string | null;
|
|
75
|
+
error: string | null;
|
|
76
|
+
trustedDevicesEnabled: boolean;
|
|
77
|
+
trustedDevicesRevoked: string | null;
|
|
78
|
+
sessionsSupported: boolean;
|
|
79
|
+
sessions: Array<{
|
|
80
|
+
loginTs: string;
|
|
81
|
+
browser: string;
|
|
82
|
+
os: string;
|
|
83
|
+
ip: string;
|
|
84
|
+
location: string;
|
|
85
|
+
}>;
|
|
86
|
+
exportSupported: boolean;
|
|
87
|
+
deletionSupported: boolean;
|
|
88
|
+
deleteError: string | null;
|
|
89
|
+
messages: AuthMessages;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Props da tela `account/mfa` (TOTP + passkeys). As props VARIAM por action, e é
|
|
93
|
+
* por isso que as específicas de passo são opcionais:
|
|
94
|
+
*
|
|
95
|
+
* - `index` manda o estado base + a lista de passkeys (`passkeysSupported` /
|
|
96
|
+
* `passkeys`).
|
|
97
|
+
* - `enroll` (após `POST /mfa/enroll`) acrescenta o passo do QR: `enrolling: true`,
|
|
98
|
+
* `secret` (base32 para entrada manual) e `qrDataUrl` (data-URL do `otpauth://`).
|
|
99
|
+
* - `confirm` com código inválido reexibe `enrolling: true` + `error`, com
|
|
100
|
+
* `secret`/`qrDataUrl` `null` (o segredo pendente NÃO é regenerado).
|
|
101
|
+
*
|
|
102
|
+
* - `enabled`: `true` quando o TOTP já está confirmado.
|
|
103
|
+
* - `recoveryCodes`: códigos recém-gerados (exibidos UMA vez) ou `null`.
|
|
104
|
+
* - `messages`: catálogo i18n (injetado pelo renderer).
|
|
105
|
+
*/
|
|
106
|
+
export interface AccountMfaProps {
|
|
107
|
+
csrfToken: string;
|
|
108
|
+
enabled: boolean;
|
|
109
|
+
recoveryCodes: string[] | null;
|
|
110
|
+
/** Presente em `index`: `true` quando o store persiste credenciais WebAuthn. */
|
|
111
|
+
passkeysSupported?: boolean;
|
|
112
|
+
/** Presente em `index`: passkeys cadastradas (vazio quando não suportado). */
|
|
113
|
+
passkeys?: PasskeySummary[];
|
|
114
|
+
/** Passo de enroll/confirm: `true` mostra QR/segredo + campo de código. */
|
|
115
|
+
enrolling?: boolean;
|
|
116
|
+
/** Segredo TOTP (base32) no passo de enroll; `null` na reexibição do confirm. */
|
|
117
|
+
secret?: string | null;
|
|
118
|
+
/** QR do `otpauth://` como data-URL no passo de enroll; `null` na reexibição. */
|
|
119
|
+
qrDataUrl?: string | null;
|
|
120
|
+
/** Erro localizado (ex.: código TOTP inválido no confirm). */
|
|
121
|
+
error?: string;
|
|
122
|
+
messages: AuthMessages;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Um método de sudo, como a tela `account/confirm` o recebe: o descritor do SPI
|
|
126
|
+
* (`SudoMethodDescriptor`) acrescido do `id` estável do método. A tela renderiza
|
|
127
|
+
* por `kind` (`form`/`action`/`redirect`/`webauthn`); `endpoint` é o POST de
|
|
128
|
+
* verificação (para `webauthn`, as options ficam em `${endpoint}/options`).
|
|
129
|
+
*/
|
|
130
|
+
export type AccountConfirmMethod = {
|
|
131
|
+
id: string;
|
|
132
|
+
} & SudoMethodDescriptor;
|
|
133
|
+
/**
|
|
134
|
+
* Props da tela `account/confirm` (sudo — confirmar identidade).
|
|
135
|
+
*
|
|
136
|
+
* - `csrfToken`: token para o POST de cada método.
|
|
137
|
+
* - `returnTo`: caminho interno de destino após confirmar (validado) ou `null`.
|
|
138
|
+
* - `error`: flash de erro da última tentativa ou `null`.
|
|
139
|
+
* - `notice`: flash informativo (ex.: "link de confirmação enviado") ou `null`.
|
|
140
|
+
* - `methods`: métodos de sudo disponíveis para a conta (ver {@link AccountConfirmMethod}).
|
|
141
|
+
* - `preferredId`: `id` do último método usado (destaque na UI) ou `null`.
|
|
142
|
+
* - `messages`: catálogo i18n (injetado pelo renderer).
|
|
143
|
+
*/
|
|
144
|
+
export interface AccountConfirmProps {
|
|
145
|
+
csrfToken: string;
|
|
146
|
+
returnTo: string | null;
|
|
147
|
+
error: string | null;
|
|
148
|
+
notice: string | null;
|
|
149
|
+
methods: AccountConfirmMethod[];
|
|
150
|
+
preferredId: string | null;
|
|
151
|
+
messages: AuthMessages;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Props da tela `account/email-confirmed` (terminal do link de troca de e-mail).
|
|
155
|
+
*
|
|
156
|
+
* - `ok`: `true` quando o token era válido e o novo e-mail foi aplicado; `false`
|
|
157
|
+
* para token inválido/expirado ou store sem suporte.
|
|
158
|
+
* - `messages`: catálogo i18n (injetado pelo renderer).
|
|
159
|
+
*/
|
|
160
|
+
export interface AccountEmailConfirmedProps {
|
|
161
|
+
ok: boolean;
|
|
162
|
+
messages: AuthMessages;
|
|
163
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos de props das telas do console de conta (`/account/*`) — a FONTE ÚNICA da
|
|
3
|
+
* verdade sobre o shape que cada página React recebe.
|
|
4
|
+
*
|
|
5
|
+
* ── Por que esses tipos existem ──────────────────────────────────────────────
|
|
6
|
+
* Um host que cria as telas do console em React próprio (via `inertiaRenderer`,
|
|
7
|
+
* em vez das views Edge built-in) precisa tipar o componente da página. Antes
|
|
8
|
+
* disso, o único "contrato" era o DOCBLOCK do `inertiaRenderer`, copiado à mão —
|
|
9
|
+
* frágil: um docblock desatualizado já enganou um host. Agora o shape vem daqui,
|
|
10
|
+
* e os CONTROLLERS REAIS (`account_session_controller`, `account_security_-
|
|
11
|
+
* controller`, `account_mfa_controller`, `account_confirm_controller`) satisfazem
|
|
12
|
+
* (`satisfies Omit<…, 'messages'>`) exatamente estes tipos ao chamar `render()`.
|
|
13
|
+
* Se o payload de um controller divergir do tipo — campo a mais, a menos, tipo
|
|
14
|
+
* trocado — o `tsc` do pacote quebra. Esse é o objetivo: um único ponto muda, e
|
|
15
|
+
* o compilador força o outro a acompanhar.
|
|
16
|
+
*
|
|
17
|
+
* ── Sobre a prop `messages` ──────────────────────────────────────────────────
|
|
18
|
+
* `messages` (catálogo i18n) é injetada pelo `inertiaRenderer` como shared prop,
|
|
19
|
+
* NÃO pelos controllers. Por isso ela faz parte destes tipos (é o que a página
|
|
20
|
+
* React recebe), mas os controllers satisfazem `Omit<…, 'messages'>` — eles nunca
|
|
21
|
+
* passam `messages` no literal do `render()`.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|