@adonis-agora/authkit-server 0.69.0 → 0.70.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/commands/import_users.js +6 -1
- package/build/index.d.ts +3 -1
- package/build/index.js +5 -1
- package/build/src/accounts/account_store.d.ts +40 -0
- package/build/src/accounts/account_store.js +20 -0
- package/build/src/accounts/lucid_store/mfa.js +22 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/audit/audit_sink.js +4 -0
- package/build/src/commands/import_users.d.ts +7 -0
- package/build/src/commands/import_users.js +15 -3
- package/build/src/define_config.d.ts +16 -0
- package/build/src/define_config.js +1 -0
- package/build/src/host/account_api/account_api_controller.d.ts +2 -0
- package/build/src/host/account_api/account_api_controller.js +22 -3
- package/build/src/host/account_api/account_mfa_api_controller.d.ts +101 -0
- package/build/src/host/account_api/account_mfa_api_controller.js +286 -0
- package/build/src/host/account_api/account_orgs_api_controller.d.ts +126 -0
- package/build/src/host/account_api/account_orgs_api_controller.js +468 -0
- package/build/src/host/account_lockout.js +7 -2
- package/build/src/host/admin_api/admin_users_service.js +13 -3
- package/build/src/host/admin_api/dto.d.ts +1 -1
- package/build/src/host/admin_validators.d.ts +2 -2
- package/build/src/host/admin_validators.js +3 -2
- package/build/src/host/controllers/account_mfa_controller.js +1 -2
- package/build/src/host/controllers/account_orgs_controller.js +4 -18
- package/build/src/host/controllers/account_security_controller.js +3 -1
- package/build/src/host/controllers/account_session_controller.js +13 -6
- package/build/src/host/controllers/interaction_controller.d.ts +10 -0
- package/build/src/host/controllers/interaction_controller.js +93 -34
- package/build/src/host/controllers/registration_controller.js +35 -9
- package/build/src/host/controllers/social_controller.js +11 -2
- package/build/src/host/email_identifier.d.ts +92 -0
- package/build/src/host/email_identifier.js +234 -0
- package/build/src/host/org_policy.d.ts +26 -0
- package/build/src/host/org_policy.js +35 -0
- package/build/src/host/passkey_registration_challenge.d.ts +12 -0
- package/build/src/host/passkey_registration_challenge.js +12 -0
- package/build/src/host/register_auth_host.js +51 -0
- package/build/src/host/sudo_mode.d.ts +17 -0
- package/build/src/host/sudo_mode.js +31 -12
- package/build/src/host/ui-dist/assets/{index-D9CYQnZR.js → index-Dct63ai-.js} +2 -2
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/host/validators.d.ts +5 -5
- package/build/src/host/validators.js +17 -5
- package/package.json +2 -2
|
@@ -53,7 +53,12 @@ export default class AuthkitImportUsers extends BaseCommand {
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
const { records, parseErrors } = parseImportFile(content);
|
|
56
|
-
const report = await importUsers(store, records, {
|
|
56
|
+
const report = await importUsers(store, records, {
|
|
57
|
+
dryRun: this.dryRun,
|
|
58
|
+
// Ponte legada do config (default ligada) — a checagem de duplicado
|
|
59
|
+
// enxerga as contas gravadas com o endereço mutilado pelo cadastro antigo.
|
|
60
|
+
legacyFallback: authkitConfig?.login?.legacyEmailFallback ?? true,
|
|
61
|
+
});
|
|
57
62
|
// Erros de parsing entram no relatório agregado.
|
|
58
63
|
report.errors.unshift(...parseErrors);
|
|
59
64
|
if (this.dryRun) {
|
package/build/index.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROU
|
|
|
64
64
|
export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
|
|
65
65
|
export type { AuthkitCsrfOptions } from './src/host/csrf.js';
|
|
66
66
|
export { authkitCsrfExceptions } from './src/host/csrf.js';
|
|
67
|
+
export type { EmailIdentifierLookup, ResolvedEmailIdentifier, } from './src/host/email_identifier.js';
|
|
68
|
+
export { legacyNormalizeEmailIdentifier, normalizeEmailIdentifier, resolveEmailIdentifier, } from './src/host/email_identifier.js';
|
|
67
69
|
export type { ResolveGeo } from './src/host/geo.js';
|
|
68
70
|
export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
|
|
69
71
|
export type { AuthMessages, I18nConfig } from './src/host/i18n.js';
|
|
@@ -94,7 +96,7 @@ export { sudoMethods } from './src/host/sudo/index.js';
|
|
|
94
96
|
export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
|
|
95
97
|
export type { SudoContext, SudoMethod, SudoMethodDescriptor, SudoRouteHelpers, } from './src/host/sudo/types.js';
|
|
96
98
|
export type { ResolvedSudoModeSetting, SudoModeSetting, } from './src/host/sudo_mode.js';
|
|
97
|
-
export { isSudoActive, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
|
|
99
|
+
export { isSudoActive, isSudoSatisfied, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
|
|
98
100
|
export { barChartSvg } from './src/host/svg_chart.js';
|
|
99
101
|
export type { ResolvedTrustedDevicesConfig, TrustedDevicePayload, TrustedDevicesConfigInput, } from './src/host/trusted_device.js';
|
|
100
102
|
export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
|
package/build/index.js
CHANGED
|
@@ -49,6 +49,7 @@ export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.
|
|
|
49
49
|
export { deriveLockedSettingKeys, isSettingLocked, lockedSettingKeys, POLICY_ROUTE_OPTIONS, resetLockedSettingKeys, SettingLockedError, setLockedSettingKeys, } from './src/host/config_locks.js';
|
|
50
50
|
export { consoleLoginUrl, getAccountId, hasAccountSession, realAccountId, } from './src/host/console_session.js';
|
|
51
51
|
export { authkitCsrfExceptions } from './src/host/csrf.js';
|
|
52
|
+
export { legacyNormalizeEmailIdentifier, normalizeEmailIdentifier, resolveEmailIdentifier, } from './src/host/email_identifier.js';
|
|
52
53
|
export { GEO_RESOLVE_TIMEOUT_MS, resolveGeoSafe } from './src/host/geo.js';
|
|
53
54
|
export { BUILTIN_MESSAGES, DEFAULT_LOCALE, DEFAULT_MESSAGES, PT_BR_MESSAGES, resolveMessages, translate, } from './src/host/i18n.js';
|
|
54
55
|
export { ensureConsoleSession } from './src/host/idp_session_bridge.js';
|
|
@@ -90,7 +91,10 @@ export { sudoMethods } from './src/host/sudo/index.js';
|
|
|
90
91
|
// `returnTo` montado na mão é um open redirect esperando acontecer.
|
|
91
92
|
export { completeSudo, fail as failSudo, LAST_METHOD_SESSION_KEY, sudoContextFrom, } from './src/host/sudo/runtime.js';
|
|
92
93
|
// Sudo mode — helpers for host controllers that require step-up authentication.
|
|
93
|
-
export { isSudoActive,
|
|
94
|
+
export { isSudoActive,
|
|
95
|
+
// A DECISÃO de sudo sem efeito na resposta — o que o host chama quando quer
|
|
96
|
+
// recusar em JSON em vez de redirecionar para `/account/confirm`.
|
|
97
|
+
isSudoSatisfied, markSudo, requireSudo, resolveEffectiveSudoMode, SUDO_ACCOUNT_SESSION_KEY, SUDO_MODE_DEFAULTS, SUDO_SESSION_KEY, } from './src/host/sudo_mode.js';
|
|
94
98
|
export { barChartSvg } from './src/host/svg_chart.js';
|
|
95
99
|
export { buildTrustedDevicePayload, isTrustedDeviceValid, resolveTrustedDevices, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
|
|
96
100
|
export { parseUserAgent } from './src/host/user_agent.js';
|
|
@@ -278,6 +278,26 @@ export interface MfaCapability {
|
|
|
278
278
|
consumeRecoveryCode(accountId: string, code: string): Promise<boolean>;
|
|
279
279
|
/** Desliga o MFA: limpa segredo + mfaEnabledAt + recovery codes. */
|
|
280
280
|
disableMfa(accountId: string): Promise<void>;
|
|
281
|
+
/**
|
|
282
|
+
* Quantos recovery codes AINDA não foram consumidos. OPCIONAL: existe para a
|
|
283
|
+
* tela de segundo fator dizer "restam N" sem revelar os códigos (só o hash
|
|
284
|
+
* fica guardado). `null` quando o MFA não está ativo.
|
|
285
|
+
*
|
|
286
|
+
* Opcional de propósito: um store de terceiro escrito contra a versão
|
|
287
|
+
* anterior de {@link MfaCapability} continua satisfazendo o tipo, e os
|
|
288
|
+
* callers fazem capability-probe (`typeof store.countRecoveryCodes ===
|
|
289
|
+
* 'function'`) antes de chamar.
|
|
290
|
+
*/
|
|
291
|
+
countRecoveryCodes?(accountId: string): Promise<number | null>;
|
|
292
|
+
/**
|
|
293
|
+
* Regenera os recovery codes de uma conta com MFA ATIVO: descarta os antigos
|
|
294
|
+
* (inclusive os não usados) e devolve os novos em claro — uma única vez, como
|
|
295
|
+
* no {@link MfaCapability.confirmTotpEnrollment}. `null` quando o MFA não
|
|
296
|
+
* está ativo (não há o que regenerar).
|
|
297
|
+
*
|
|
298
|
+
* Opcional pelo mesmo motivo de {@link MfaCapability.countRecoveryCodes}.
|
|
299
|
+
*/
|
|
300
|
+
regenerateRecoveryCodes?(accountId: string): Promise<string[] | null>;
|
|
281
301
|
}
|
|
282
302
|
/**
|
|
283
303
|
* MFA / WebAuthn (passkeys) — 2º fator alternativo ao TOTP. Como o TOTP, é uma
|
|
@@ -654,6 +674,26 @@ export declare function supportsCountByGlobalRole(store: AccountStore): store is
|
|
|
654
674
|
};
|
|
655
675
|
/** Type guard: o store implementa a capacidade de passkeys / WebAuthn. */
|
|
656
676
|
export declare function supportsPasskeys(store: AccountStore): store is AccountStore & WebauthnCapability;
|
|
677
|
+
/**
|
|
678
|
+
* Type guard: o store sabe REGENERAR recovery codes
|
|
679
|
+
* ({@link MfaCapability.regenerateRecoveryCodes}).
|
|
680
|
+
*
|
|
681
|
+
* Guard PRÓPRIO, separado de {@link supportsMfa}, porque o método é opcional
|
|
682
|
+
* dentro da capacidade: um store escrito antes desta versão implementa MFA
|
|
683
|
+
* inteiro e não implementa este método. Quem oferece o botão "gerar novos
|
|
684
|
+
* códigos" pergunta por aqui.
|
|
685
|
+
*/
|
|
686
|
+
export declare function supportsRecoveryCodeRegeneration(store: AccountStore): store is AccountStore & MfaCapability & {
|
|
687
|
+
regenerateRecoveryCodes(accountId: string): Promise<string[] | null>;
|
|
688
|
+
};
|
|
689
|
+
/**
|
|
690
|
+
* Type guard: o store sabe CONTAR recovery codes restantes
|
|
691
|
+
* ({@link MfaCapability.countRecoveryCodes}). Mesmo motivo do
|
|
692
|
+
* {@link supportsRecoveryCodeRegeneration} para ser um guard separado.
|
|
693
|
+
*/
|
|
694
|
+
export declare function supportsRecoveryCodeCount(store: AccountStore): store is AccountStore & MfaCapability & {
|
|
695
|
+
countRecoveryCodes(accountId: string): Promise<number | null>;
|
|
696
|
+
};
|
|
657
697
|
/** Type guard: o store implementa account linking por identidade de provider. */
|
|
658
698
|
export declare function supportsProviderIdentity(store: AccountStore): store is AccountStore & ProviderIdentityCapability;
|
|
659
699
|
/** Type guard: o store implementa o self-service de segurança (senha/e-mail). */
|
|
@@ -18,6 +18,26 @@ export function supportsCountByGlobalRole(store) {
|
|
|
18
18
|
export function supportsPasskeys(store) {
|
|
19
19
|
return typeof store.listPasskeys === 'function';
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Type guard: o store sabe REGENERAR recovery codes
|
|
23
|
+
* ({@link MfaCapability.regenerateRecoveryCodes}).
|
|
24
|
+
*
|
|
25
|
+
* Guard PRÓPRIO, separado de {@link supportsMfa}, porque o método é opcional
|
|
26
|
+
* dentro da capacidade: um store escrito antes desta versão implementa MFA
|
|
27
|
+
* inteiro e não implementa este método. Quem oferece o botão "gerar novos
|
|
28
|
+
* códigos" pergunta por aqui.
|
|
29
|
+
*/
|
|
30
|
+
export function supportsRecoveryCodeRegeneration(store) {
|
|
31
|
+
return typeof store.regenerateRecoveryCodes === 'function';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Type guard: o store sabe CONTAR recovery codes restantes
|
|
35
|
+
* ({@link MfaCapability.countRecoveryCodes}). Mesmo motivo do
|
|
36
|
+
* {@link supportsRecoveryCodeRegeneration} para ser um guard separado.
|
|
37
|
+
*/
|
|
38
|
+
export function supportsRecoveryCodeCount(store) {
|
|
39
|
+
return typeof store.countRecoveryCodes === 'function';
|
|
40
|
+
}
|
|
21
41
|
/** Type guard: o store implementa account linking por identidade de provider. */
|
|
22
42
|
export function supportsProviderIdentity(store) {
|
|
23
43
|
return typeof store.findByProviderIdentity === 'function';
|
|
@@ -100,6 +100,28 @@ export function buildMfa(ctx) {
|
|
|
100
100
|
await repo.upsert(accountId, { recoveryCodes: remaining });
|
|
101
101
|
return true;
|
|
102
102
|
},
|
|
103
|
+
async countRecoveryCodes(accountId) {
|
|
104
|
+
const state = await repo.read(accountId);
|
|
105
|
+
// Sem MFA ativo não há conjunto de códigos a contar — `null` diz "não se
|
|
106
|
+
// aplica", diferente de `0` ("acabaram, gere novos").
|
|
107
|
+
if (!state || !state.mfaEnabledAt)
|
|
108
|
+
return null;
|
|
109
|
+
return Array.isArray(state.recoveryCodes) ? state.recoveryCodes.length : 0;
|
|
110
|
+
},
|
|
111
|
+
async regenerateRecoveryCodes(accountId) {
|
|
112
|
+
const state = await repo.read(accountId);
|
|
113
|
+
// Só regenera sobre MFA JÁ ATIVO. Num enrollment pendente os códigos saem
|
|
114
|
+
// do `confirmTotpEnrollment`; deixar regenerar antes disso criaria um
|
|
115
|
+
// conjunto válido de credenciais de recuperação para um fator que o
|
|
116
|
+
// usuário ainda não provou possuir.
|
|
117
|
+
if (!state || !state.mfaEnabledAt)
|
|
118
|
+
return null;
|
|
119
|
+
const codes = Array.from({ length: recoveryCodeCount }, () => generateRecoveryCode());
|
|
120
|
+
// Substitui o conjunto inteiro: os antigos (inclusive os não usados)
|
|
121
|
+
// param de valer no mesmo instante — é esse o ponto de "regenerar".
|
|
122
|
+
await repo.upsert(accountId, { recoveryCodes: codes.map(sha256) });
|
|
123
|
+
return codes;
|
|
124
|
+
},
|
|
103
125
|
async disableMfa(accountId) {
|
|
104
126
|
// Limpa todo o estado de MFA. Inclui o anti-replay: um futuro re-enroll começa do zero.
|
|
105
127
|
await repo.clear(accountId);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* varre os `audit.record({ type: ... })` do `src/` e falha quando um evento
|
|
9
9
|
* emitido não está aqui.
|
|
10
10
|
*/
|
|
11
|
-
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', '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', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated'];
|
|
11
|
+
export declare const AUDIT_EVENT_TYPES: readonly ['login.success', 'login.failure', 'signup', 'password_reset.issued', 'password_reset.consumed', 'pat.issued', 'pat.revoked', 'pat.used', 'impersonation', 'impersonation.panel_viewed', 'impersonation.stopped', 'mfa.enabled', 'mfa.disabled', 'mfa.recovery_codes_regenerated', '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', 'login.magic_link_sent', 'session.revoked', 'account.signed_out_all', 'client.secret_regenerated', 'roles_catalog.updated'];
|
|
12
12
|
/** Tipos de eventos de auditoria relevantes para segurança emitidos pelo IdP. */
|
|
13
13
|
export type AuditEventType = (typeof AUDIT_EVENT_TYPES)[number];
|
|
14
14
|
/**
|
|
@@ -29,6 +29,10 @@ export const AUDIT_EVENT_TYPES = [
|
|
|
29
29
|
'impersonation.stopped',
|
|
30
30
|
'mfa.enabled',
|
|
31
31
|
'mfa.disabled',
|
|
32
|
+
// Os recovery codes foram TROCADOS: os antigos (inclusive os não usados)
|
|
33
|
+
// deixaram de valer no mesmo instante. Evento próprio, e não `mfa.enabled`,
|
|
34
|
+
// porque o fator não mudou — mudou o conjunto de credenciais de contorno.
|
|
35
|
+
'mfa.recovery_codes_regenerated',
|
|
32
36
|
'account.locked',
|
|
33
37
|
'passkey.registered',
|
|
34
38
|
'passkey.removed',
|
|
@@ -42,10 +42,17 @@ export declare function parseImportFile(content: string): {
|
|
|
42
42
|
*
|
|
43
43
|
* Lógica PURA quanto a I/O de arquivo (recebe os registros já parseados) — fácil
|
|
44
44
|
* de testar. Lança se o store não suporta import nem create.
|
|
45
|
+
*
|
|
46
|
+
* `legacyFallback` (default ligado, como no resto da lib) faz a checagem de
|
|
47
|
+
* duplicado enxergar também a conta que o cadastro antigo gravou com o endereço
|
|
48
|
+
* mutilado — importar a grafia REAL do dono dela pula como duplicado em vez de
|
|
49
|
+
* criar uma SEGUNDA conta. O comando não tem acesso ao config, então o flag
|
|
50
|
+
* chega por aqui.
|
|
45
51
|
*/
|
|
46
52
|
export declare function importUsers(store: AccountStore, records: {
|
|
47
53
|
line: number;
|
|
48
54
|
record: ImportUserRecord;
|
|
49
55
|
}[], options?: {
|
|
50
56
|
dryRun?: boolean;
|
|
57
|
+
legacyFallback?: boolean;
|
|
51
58
|
}): Promise<ImportReport>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { supportsAccountImport } from '../accounts/account_store.js';
|
|
2
|
+
import { normalizeEmailIdentifier, resolveEmailIdentifier } from '../host/email_identifier.js';
|
|
2
3
|
/**
|
|
3
4
|
* Faz o parse do conteúdo do arquivo: aceita NDJSON (uma linha JSON por usuário)
|
|
4
5
|
* OU um array JSON. Retorna os registros + os erros de parsing por linha. PURO
|
|
@@ -47,18 +48,29 @@ export function parseImportFile(content) {
|
|
|
47
48
|
*
|
|
48
49
|
* Lógica PURA quanto a I/O de arquivo (recebe os registros já parseados) — fácil
|
|
49
50
|
* de testar. Lança se o store não suporta import nem create.
|
|
51
|
+
*
|
|
52
|
+
* `legacyFallback` (default ligado, como no resto da lib) faz a checagem de
|
|
53
|
+
* duplicado enxergar também a conta que o cadastro antigo gravou com o endereço
|
|
54
|
+
* mutilado — importar a grafia REAL do dono dela pula como duplicado em vez de
|
|
55
|
+
* criar uma SEGUNDA conta. O comando não tem acesso ao config, então o flag
|
|
56
|
+
* chega por aqui.
|
|
50
57
|
*/
|
|
51
58
|
export async function importUsers(store, records, options = {}) {
|
|
52
59
|
const report = { created: 0, skippedDuplicate: 0, errors: [] };
|
|
53
60
|
const canImport = supportsAccountImport(store);
|
|
54
61
|
for (const { line, record } of records) {
|
|
55
|
-
|
|
62
|
+
// MESMA normalização do cadastro/login (`trim` + `toLowerCase`) — sem ela o
|
|
63
|
+
// import gravava `Davi@Acme.com` e o login (normalizado) não achava a conta.
|
|
64
|
+
const email = normalizeEmailIdentifier(record.email);
|
|
56
65
|
if (!email) {
|
|
57
66
|
report.errors.push({ line, reason: 'missing email' });
|
|
58
67
|
continue;
|
|
59
68
|
}
|
|
60
|
-
// Duplicado: e-mail já existe → pula.
|
|
61
|
-
|
|
69
|
+
// Duplicado: e-mail já existe → pula. Enxerga também a conta gravada com o
|
|
70
|
+
// endereço mutilado (ponte legada) — empate segue criando, como no cadastro.
|
|
71
|
+
const existing = (await resolveEmailIdentifier(store, record.email, {
|
|
72
|
+
legacyFallback: options.legacyFallback,
|
|
73
|
+
})).account;
|
|
62
74
|
if (existing) {
|
|
63
75
|
report.skippedDuplicate++;
|
|
64
76
|
continue;
|
|
@@ -537,10 +537,26 @@ export interface LoginConfigInput {
|
|
|
537
537
|
* idêntico ao de antes, e-mail idêntico). Ver `host/otp_login.ts`.
|
|
538
538
|
*/
|
|
539
539
|
otp?: OtpLoginConfigInput;
|
|
540
|
+
/**
|
|
541
|
+
* PONTE TEMPORÁRIA: no passo de identificador, quando o e-mail normalizado
|
|
542
|
+
* (`trim` + `toLowerCase`) não acha conta nenhuma, tenta também o endereço
|
|
543
|
+
* exatamente como foi digitado e a normalização LEGADA (os defaults do
|
|
544
|
+
* validator.js que o cadastro aplicava até a v0.68 e que, no gmail, removiam
|
|
545
|
+
* pontos e sub-endereço `+tag`). Só aceita quando essas formas apontam para
|
|
546
|
+
* EXATAMENTE UMA conta — empate é tratado como "não achei".
|
|
547
|
+
*
|
|
548
|
+
* Existe para que as contas que nasceram com o endereço mutilado
|
|
549
|
+
* (`davi.carvalho96@gmail.com` gravado como `davicarvalho96@gmail.com`)
|
|
550
|
+
* continuem tendo caminho de volta. Default: `true` — desligar tranca essas
|
|
551
|
+
* contas do lado de fora. Depois de migrar os endereços gravados, desligue e,
|
|
552
|
+
* mais adiante, a ponte inteira sai da lib (ver `host/email_identifier.ts`).
|
|
553
|
+
*/
|
|
554
|
+
legacyEmailFallback?: boolean;
|
|
540
555
|
}
|
|
541
556
|
export interface ResolvedLoginConfig {
|
|
542
557
|
requireVerifiedEmail: boolean;
|
|
543
558
|
otp: ResolvedOtpLoginConfig;
|
|
559
|
+
legacyEmailFallback: boolean;
|
|
544
560
|
}
|
|
545
561
|
export declare function resolveLogin(input?: LoginConfigInput): ResolvedLoginConfig;
|
|
546
562
|
/**
|
|
@@ -137,6 +137,7 @@ export function resolveLogin(input) {
|
|
|
137
137
|
return {
|
|
138
138
|
requireVerifiedEmail: input?.requireVerifiedEmail ?? false,
|
|
139
139
|
otp: resolveOtpLoginConfig(input?.otp),
|
|
140
|
+
legacyEmailFallback: input?.legacyEmailFallback ?? true,
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
143
|
export function resolveRegistration(input) {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* GET /account/api/orgs/invitations → convites pendentes
|
|
29
29
|
*/
|
|
30
30
|
import '../augmentations.js';
|
|
31
|
-
import { supportsAccountSecurity, supportsLoginMethodsPreference, supportsMagicLink, supportsOrganizations, supportsPasskeys, supportsProfile, } from '../../accounts/account_store.js';
|
|
31
|
+
import { supportsAccountSecurity, supportsLoginMethodsPreference, supportsMagicLink, supportsOrganizations, supportsPasskeys, supportsProfile, supportsRecoveryCodeCount, supportsRecoveryCodeRegeneration, } from '../../accounts/account_store.js';
|
|
32
32
|
import { PasswordPolicyError } from '../../password/password_manager.js';
|
|
33
33
|
import { accountPath } from '../account_paths.js';
|
|
34
34
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
@@ -627,6 +627,16 @@ export default class AccountApiController {
|
|
|
627
627
|
/* fail-safe */
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
|
+
// Quantos recovery codes restam (capability-probed, fail-safe).
|
|
631
|
+
let recoveryRemaining = null;
|
|
632
|
+
if (supportsRecoveryCodeCount(cfg.accountStore)) {
|
|
633
|
+
try {
|
|
634
|
+
recoveryRemaining = await cfg.accountStore.countRecoveryCodes(userId);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
/* fail-safe */
|
|
638
|
+
}
|
|
639
|
+
}
|
|
630
640
|
return {
|
|
631
641
|
enabled,
|
|
632
642
|
totp: { enrolled: enabled },
|
|
@@ -639,8 +649,17 @@ export default class AccountApiController {
|
|
|
639
649
|
createdAt: p.createdAt ?? null,
|
|
640
650
|
})),
|
|
641
651
|
},
|
|
642
|
-
|
|
643
|
-
|
|
652
|
+
recovery: {
|
|
653
|
+
// Os códigos em si NUNCA voltam aqui — só no instante em que são
|
|
654
|
+
// criados (`/mfa/totp/confirm` ou `/mfa/recovery-codes`), porque é só
|
|
655
|
+
// o hash deles que fica guardado.
|
|
656
|
+
available: enabled,
|
|
657
|
+
// `remaining` é `null` quando o store não sabe contar (capacidade
|
|
658
|
+
// opcional) — diferente de `0`, que significa "acabaram, gere novos".
|
|
659
|
+
remaining: recoveryRemaining,
|
|
660
|
+
// O host só mostra o botão "gerar novos códigos" se houver como.
|
|
661
|
+
regenerable: supportsRecoveryCodeRegeneration(cfg.accountStore),
|
|
662
|
+
},
|
|
644
663
|
};
|
|
645
664
|
}
|
|
646
665
|
// ─── GET /account/api/login-methods ─────────────────────────────────────
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account Self-Service JSON API — SEGUNDO FATOR (TOTP + passkeys)
|
|
3
|
+
*
|
|
4
|
+
* Espelho JSON do `AccountMfaController`, para o host que desenha a própria
|
|
5
|
+
* tela de "segundo fator" e não pode navegar o browser no meio do fluxo.
|
|
6
|
+
*
|
|
7
|
+
* Mapa de rotas (sob o `accountGuard`; mutantes sob o CSRF do shield do host):
|
|
8
|
+
* POST /account/api/mfa/totp/enroll → inicia o enrolamento (sudo)
|
|
9
|
+
* POST /account/api/mfa/totp/confirm → confirma com o código (throttled)
|
|
10
|
+
* POST /account/api/mfa/totp/disable → desliga o MFA (sudo)
|
|
11
|
+
* POST /account/api/mfa/recovery-codes → regenera os códigos (sudo)
|
|
12
|
+
* POST /account/api/mfa/passkeys/options → options da cerimônia de registro
|
|
13
|
+
* POST /account/api/mfa/passkeys/verify → verifica o attestation (sudo)
|
|
14
|
+
*
|
|
15
|
+
* Paridade de gates com o console HTML, endpoint a endpoint:
|
|
16
|
+
*
|
|
17
|
+
* | ação | console HTML | aqui |
|
|
18
|
+
* | ---------------- | ------------ | ---------------- |
|
|
19
|
+
* | enroll | sudo | sudo → 403 JSON |
|
|
20
|
+
* | confirm | sem sudo | sem sudo |
|
|
21
|
+
* | disable | sudo | sudo → 403 JSON |
|
|
22
|
+
* | passkey options | sem sudo | sem sudo |
|
|
23
|
+
* | passkey verify | sudo | sudo → 403 JSON |
|
|
24
|
+
* | recovery codes | (não existe) | sudo → 403 JSON |
|
|
25
|
+
*
|
|
26
|
+
* A única diferença é a FORMA da recusa: `requireSudo` devolve um redirect para
|
|
27
|
+
* `/account/confirm`, que numa SPA chega como uma página HTML onde ela esperava
|
|
28
|
+
* JSON. Aqui a recusa vira `403 { error: { code: 'sudo_required' } }` e a tela
|
|
29
|
+
* do host manda o usuário confirmar identidade por conta própria. Mesma
|
|
30
|
+
* política, resposta legível por máquina.
|
|
31
|
+
*
|
|
32
|
+
* `confirm` NÃO exige sudo — porque o `enroll` que criou o segredo pendente já
|
|
33
|
+
* exigiu, e pedir de novo no passo seguinte quebraria o enrolamento de quem
|
|
34
|
+
* demorou a digitar o código. É exatamente o que o console faz. O que o console
|
|
35
|
+
* não tem, e aqui existe, é o THROTTLE: o código de 6 dígitos é adivinhável, e
|
|
36
|
+
* a rota carrega o bucket de sudo (por IP) — mais apertado que o form, nunca
|
|
37
|
+
* mais frouxo.
|
|
38
|
+
*
|
|
39
|
+
* Os dois caminhos (clássico e JSON) COMPARTILHAM o slot do desafio WebAuthn
|
|
40
|
+
* (`PASSKEY_REG_CHALLENGE_KEY`), então um `options` pedido num deles pode ser
|
|
41
|
+
* finalizado pelo outro e nunca existem dois desafios vivos na mesma sessão.
|
|
42
|
+
*/
|
|
43
|
+
import '../augmentations.js';
|
|
44
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
45
|
+
export default class AccountMfaApiController {
|
|
46
|
+
#private;
|
|
47
|
+
/**
|
|
48
|
+
* Inicia o enrolamento TOTP: segredo pendente + `otpauth://` URI + o QR já
|
|
49
|
+
* renderizado como data-URL (o console renderiza server-side; aqui a tela do
|
|
50
|
+
* host recebe pronto e decide se mostra o QR, o segredo, ou os dois).
|
|
51
|
+
*/
|
|
52
|
+
enrollTotp(ctx: HttpContext): Promise<void | {
|
|
53
|
+
secret: string;
|
|
54
|
+
otpauthUri: string;
|
|
55
|
+
qrDataUrl: string;
|
|
56
|
+
}>;
|
|
57
|
+
/**
|
|
58
|
+
* Confirma o enrolamento com o código do app autenticador. Sucesso ativa o
|
|
59
|
+
* MFA e devolve os recovery codes — UMA vez, como no console (lá eles vão num
|
|
60
|
+
* flash; aqui, no corpo da resposta, que é o equivalente headless).
|
|
61
|
+
*/
|
|
62
|
+
confirmTotp(ctx: HttpContext): Promise<void | {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
enabled: boolean;
|
|
65
|
+
recoveryCodes: string[];
|
|
66
|
+
}>;
|
|
67
|
+
/** Desliga o MFA (TOTP + recovery codes). Exige sudo, como o console. */
|
|
68
|
+
disableTotp(ctx: HttpContext): Promise<{
|
|
69
|
+
ok: boolean;
|
|
70
|
+
enabled: boolean;
|
|
71
|
+
} | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* Regenera os recovery codes de uma conta com MFA ATIVO e devolve os novos —
|
|
74
|
+
* uma única vez. Exige sudo: o resultado é um conjunto de credenciais que
|
|
75
|
+
* contorna o segundo fator, então vale o mesmo gate do `enroll`/`disable`.
|
|
76
|
+
*
|
|
77
|
+
* Capability-probed: stores que não implementam
|
|
78
|
+
* `regenerateRecoveryCodes` respondem 422 em vez de 500.
|
|
79
|
+
*/
|
|
80
|
+
regenerateRecoveryCodes(ctx: HttpContext): Promise<void | {
|
|
81
|
+
ok: boolean;
|
|
82
|
+
recoveryCodes: string[];
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Options da cerimônia de REGISTRO de passkey, guardando o desafio na sessão.
|
|
86
|
+
* Sem sudo, igual ao endpoint clássico: quem paga o gate é o `verify`, que é
|
|
87
|
+
* onde a credencial passa a existir.
|
|
88
|
+
*/
|
|
89
|
+
passkeyRegisterOptions(ctx: HttpContext): Promise<any>;
|
|
90
|
+
/**
|
|
91
|
+
* Verifica o attestation contra o desafio da sessão e persiste a credencial.
|
|
92
|
+
*
|
|
93
|
+
* O endpoint clássico responde 302 numa navegação e `{ok:true}` num fetch;
|
|
94
|
+
* este responde SEMPRE JSON — inclusive na recusa de sudo, que lá é um
|
|
95
|
+
* redirect. É a diferença que faz a cerimônia caber numa SPA: o browser não
|
|
96
|
+
* pode navegar entre `startRegistration()` e a verificação.
|
|
97
|
+
*/
|
|
98
|
+
passkeyRegisterVerify(ctx: HttpContext): Promise<void | {
|
|
99
|
+
ok: boolean;
|
|
100
|
+
}>;
|
|
101
|
+
}
|