@adonis-agora/authkit-server 0.54.0 → 0.55.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/index.d.ts +4 -2
- package/build/index.js +2 -1
- package/build/providers/authkit_server_provider.js +18 -0
- package/build/src/accounts/lucid_account_store.d.ts +35 -0
- package/build/src/accounts/lucid_account_store.js +9 -0
- package/build/src/accounts/lucid_store/core.js +88 -19
- package/build/src/accounts/lucid_store/shared.d.ts +14 -0
- package/build/src/accounts/lucid_store/token_hash.d.ts +79 -0
- package/build/src/accounts/lucid_store/token_hash.js +145 -0
- package/build/src/define_config.d.ts +93 -8
- package/build/src/define_config.js +28 -2
- package/build/src/host/account_api/account_api_controller.js +5 -4
- package/build/src/host/admin_api/admin_users_service.js +2 -1
- package/build/src/host/admin_api/api_orgs_controller.js +2 -1
- package/build/src/host/admin_console/console_impersonation_controller.d.ts +15 -1
- package/build/src/host/admin_console/console_impersonation_controller.js +26 -2
- package/build/src/host/admin_console/console_orgs_controller.js +3 -1
- package/build/src/host/admin_validators.d.ts +3 -3
- package/build/src/host/auth_host_config.d.ts +30 -0
- package/build/src/host/auth_host_config.js +15 -0
- package/build/src/host/config_locks.d.ts +29 -0
- package/build/src/host/config_locks.js +46 -0
- package/build/src/host/console_session.d.ts +38 -2
- package/build/src/host/console_session.js +46 -2
- package/build/src/host/controllers/account_mfa_controller.js +5 -5
- package/build/src/host/controllers/account_orgs_controller.js +2 -1
- package/build/src/host/controllers/account_security_controller.js +6 -5
- package/build/src/host/controllers/interaction_controller.js +122 -26
- package/build/src/host/controllers/registration_controller.js +5 -4
- package/build/src/host/controllers/social_controller.js +37 -0
- package/build/src/host/default_mailer.d.ts +36 -5
- package/build/src/host/default_mailer.js +63 -10
- package/build/src/host/i18n.d.ts +10 -0
- package/build/src/host/i18n.js +16 -0
- package/build/src/host/login_attempt.d.ts +88 -2
- package/build/src/host/login_attempt.js +101 -48
- package/build/src/host/login_notify.js +2 -2
- package/build/src/host/oidc_rp_guard.d.ts +25 -2
- package/build/src/host/oidc_rp_guard.js +55 -9
- package/build/src/host/origin.d.ts +21 -0
- package/build/src/host/origin.js +22 -0
- package/build/src/host/register_auth_host.d.ts +104 -3
- package/build/src/host/register_auth_host.js +222 -26
- package/build/src/host/runtime_settings.d.ts +16 -0
- package/build/src/host/runtime_settings.js +24 -0
- package/build/src/host/runtime_toggles.d.ts +10 -0
- package/build/src/host/runtime_toggles.js +4 -0
- package/build/src/host/security_notice_service.d.ts +4 -2
- package/build/src/host/security_notice_service.js +4 -2
- package/build/src/host/sudo/index.d.ts +8 -0
- package/build/src/host/sudo/index.js +8 -0
- package/build/src/host/sudo/methods/magic_link.d.ts +17 -3
- package/build/src/host/sudo/methods/magic_link.js +37 -13
- package/build/src/host/sudo/runtime.d.ts +44 -4
- package/build/src/host/sudo/runtime.js +90 -6
- package/build/src/host/sudo/satisfiability.d.ts +62 -0
- package/build/src/host/sudo/satisfiability.js +89 -0
- package/build/src/password/common_passwords.js +27 -7
- package/build/src/provider/oidc_service.js +25 -13
- package/build/src/provider/token_exchange.d.ts +12 -1
- package/build/src/provider/token_exchange.js +12 -0
- package/package.json +6 -3
- /package/build/{password → src/password}/common_passwords.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAccountLoginUrl } from './account_login_url.js';
|
|
2
|
+
import { impersonationState } from './impersonation_session.js';
|
|
2
3
|
import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
|
|
3
4
|
/**
|
|
4
5
|
* Helpers públicos para integrar a sessão do console do AuthKit com
|
|
@@ -10,13 +11,56 @@ import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
|
|
|
10
11
|
* sessão mudar.
|
|
11
12
|
*/
|
|
12
13
|
/**
|
|
13
|
-
* Retorna o id da conta
|
|
14
|
-
* sessão).
|
|
14
|
+
* Retorna o id da conta que o request está representando no console (ou `null`
|
|
15
|
+
* quando não há sessão).
|
|
16
|
+
*
|
|
17
|
+
* ATENÇÃO: com impersonation ativa isto é a conta PERSONIFICADA, não o admin
|
|
18
|
+
* que a personificou. É o comportamento certo para "como qual conta este
|
|
19
|
+
* request está agindo?" e é a entrada ERRADA para uma checagem de
|
|
20
|
+
* permissão/role — para isso use `realAccountId`.
|
|
15
21
|
*/
|
|
16
22
|
export function getAccountId(ctx) {
|
|
17
23
|
const accountId = ctx.session?.get(ACCOUNT_SESSION_KEY);
|
|
18
24
|
return accountId ?? null;
|
|
19
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Retorna o id do HUMANO real por trás do request — o id que uma decisão de
|
|
28
|
+
* autorização deve usar.
|
|
29
|
+
*
|
|
30
|
+
* ```
|
|
31
|
+
* impersonation ativa → o id do impersonator (o admin real)
|
|
32
|
+
* sem impersonation → o id da conta logada
|
|
33
|
+
* sem sessão → null
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* USE ESTE em qualquer checagem de permissão/role ("esta pessoa é admin?",
|
|
37
|
+
* "esta pessoa pode aprovar isto?"). Use `getAccountId` só para responder
|
|
38
|
+
* "como qual conta este request está agindo?" (queries com escopo no alvo,
|
|
39
|
+
* UI, banner de impersonation).
|
|
40
|
+
*
|
|
41
|
+
* POR QUE ISSO EXISTE. Durante uma impersonation, `getAccountId` devolve a
|
|
42
|
+
* conta PERSONIFICADA — é o comportamento correto dele e o resto do app
|
|
43
|
+
* depende disso. Mas ele é, por isso mesmo, a entrada ERRADA para um role
|
|
44
|
+
* check: perguntar "o `getAccountId` é admin?" com impersonation ativa
|
|
45
|
+
* pergunta sobre o usuário personificado e, quando ele por acaso for admin,
|
|
46
|
+
* entrega os privilégios do admin a quem está sendo personificado — e, no
|
|
47
|
+
* sentido inverso, faz o admin real perder o próprio acesso. `realAccountId`
|
|
48
|
+
* ignora a troca de conta e responde sempre sobre quem está de fato dirigindo
|
|
49
|
+
* a sessão.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // gate de /admin: a pessoa por trás do request é admin?
|
|
53
|
+
* const id = realAccountId(ctx)
|
|
54
|
+
* if (!id || !(await authz.hasRole(id, 'admin'))) throw new Error('forbidden')
|
|
55
|
+
*/
|
|
56
|
+
export function realAccountId(ctx) {
|
|
57
|
+
// Sem sessão não há nem impersonation nem conta: mesma tolerância do
|
|
58
|
+
// `getAccountId` (que usa `ctx.session?.`), pois `impersonationState` assume
|
|
59
|
+
// uma sessão presente.
|
|
60
|
+
if (!ctx.session)
|
|
61
|
+
return null;
|
|
62
|
+
return impersonationState(ctx).impersonatorId ?? getAccountId(ctx);
|
|
63
|
+
}
|
|
20
64
|
/**
|
|
21
65
|
* `true` quando o request carrega uma sessão de conta do console.
|
|
22
66
|
*/
|
|
@@ -112,13 +112,13 @@ export default class AccountMfaController {
|
|
|
112
112
|
kind: 'passkey_added',
|
|
113
113
|
ip,
|
|
114
114
|
timestamp: ts,
|
|
115
|
-
}, cfg.mail, cfg.audit);
|
|
115
|
+
}, cfg.mail, cfg.audit, cfg);
|
|
116
116
|
await dispatchSecurityNotice(ctx, {
|
|
117
117
|
account: { id: userId, email: accountForNotice.email },
|
|
118
118
|
kind: 'mfa_enabled',
|
|
119
119
|
ip,
|
|
120
120
|
timestamp: ts,
|
|
121
|
-
}, cfg.mail, cfg.audit);
|
|
121
|
+
}, cfg.mail, cfg.audit, cfg);
|
|
122
122
|
}
|
|
123
123
|
// Resposta DUAL. A cerimônia WebAuthn da tela built-in (`mfa.edge`) chama
|
|
124
124
|
// este endpoint por `fetch` e só olha `res.ok` — mas um `<form>` HTML
|
|
@@ -158,7 +158,7 @@ export default class AccountMfaController {
|
|
|
158
158
|
kind: 'passkey_removed',
|
|
159
159
|
ip: ctx.request.ip?.() ?? null,
|
|
160
160
|
timestamp: new Date().toISOString(),
|
|
161
|
-
}, cfg.mail, cfg.audit);
|
|
161
|
+
}, cfg.mail, cfg.audit, cfg);
|
|
162
162
|
}
|
|
163
163
|
return ctx.response.redirect(accountPath('mfa'));
|
|
164
164
|
}
|
|
@@ -225,7 +225,7 @@ export default class AccountMfaController {
|
|
|
225
225
|
kind: 'mfa_enabled',
|
|
226
226
|
ip: ctx.request.ip?.() ?? null,
|
|
227
227
|
timestamp: new Date().toISOString(),
|
|
228
|
-
}, cfg.mail, cfg.audit);
|
|
228
|
+
}, cfg.mail, cfg.audit, cfg);
|
|
229
229
|
}
|
|
230
230
|
// Mostra os recovery codes UMA vez (flash) e volta pro estado "ativado".
|
|
231
231
|
ctx.session.flash('recoveryCodes', result.recoveryCodes ?? []);
|
|
@@ -255,7 +255,7 @@ export default class AccountMfaController {
|
|
|
255
255
|
kind: 'mfa_disabled',
|
|
256
256
|
ip: ctx.request.ip?.() ?? null,
|
|
257
257
|
timestamp: new Date().toISOString(),
|
|
258
|
-
}, cfg.mail, cfg.audit);
|
|
258
|
+
}, cfg.mail, cfg.audit, cfg);
|
|
259
259
|
}
|
|
260
260
|
return ctx.response.redirect(accountPath('mfa'));
|
|
261
261
|
}
|
|
@@ -4,6 +4,7 @@ import { getAccountLoginUrl } from '../account_login_url.js';
|
|
|
4
4
|
import { accountPath } from '../account_paths.js';
|
|
5
5
|
import { ACTIVE_ORG_COOKIE, ACTIVE_ORG_COOKIE_TTL, encodeActiveOrgCookie, } from '../active_org_cookie.js';
|
|
6
6
|
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
7
|
+
import { authkitOrigin } from '../origin.js';
|
|
7
8
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
8
9
|
import { isRoleInCatalog } from '../runtime_toggles.js';
|
|
9
10
|
/**
|
|
@@ -190,7 +191,7 @@ export default class AccountOrgsController {
|
|
|
190
191
|
// Dispara e-mail via mail hook (best-effort)
|
|
191
192
|
if (cfg.mail?.onOrgInvitation) {
|
|
192
193
|
const org = await store.findOrgById(params.id);
|
|
193
|
-
const acceptUrl = `${
|
|
194
|
+
const acceptUrl = `${authkitOrigin(cfg)}${accountPath('orgs')}/invitations/${token}/accept`;
|
|
194
195
|
try {
|
|
195
196
|
await cfg.mail.onOrgInvitation({
|
|
196
197
|
email,
|
|
@@ -11,6 +11,7 @@ import { AvatarUploadError, isAvatarUploadSupported, storeAvatar } from '../avat
|
|
|
11
11
|
import { sendEmailChangeConfirmationEmail, sendEmailChangeNoticeEmail, sendEmailChangedCompletedEmail, } from '../default_mailer.js';
|
|
12
12
|
import { translate } from '../i18n.js';
|
|
13
13
|
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
14
|
+
import { authkitOrigin } from '../origin.js';
|
|
14
15
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
15
16
|
import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '../runtime_toggles.js';
|
|
16
17
|
import { dispatchSecurityNotice } from '../security_notice_service.js';
|
|
@@ -366,7 +367,7 @@ export default class AccountSecurityController {
|
|
|
366
367
|
kind: 'password_changed',
|
|
367
368
|
ip: ctx.request.ip?.() ?? null,
|
|
368
369
|
timestamp: new Date().toISOString(),
|
|
369
|
-
}, cfg.mail, cfg.audit);
|
|
370
|
+
}, cfg.mail, cfg.audit, cfg);
|
|
370
371
|
}
|
|
371
372
|
ctx.session.flash('passwordChanged', translate(cfg.messages, 'account.security.password_changed'));
|
|
372
373
|
return ctx.response.redirect(accountPath('security'));
|
|
@@ -417,7 +418,7 @@ export default class AccountSecurityController {
|
|
|
417
418
|
email: newEmail,
|
|
418
419
|
ip: ctx.request.ip?.() ?? null,
|
|
419
420
|
});
|
|
420
|
-
const origin =
|
|
421
|
+
const origin = authkitOrigin(cfg);
|
|
421
422
|
const confirmUrl = `${origin}${accountPath('emailConfirm')}?token=${encodeURIComponent(issued.token)}`;
|
|
422
423
|
// 1. Link de confirmação → NOVO e-mail (hook onEmailChangeConfirm > onEmailVerification > default).
|
|
423
424
|
if (cfg.mail?.onEmailChangeConfirm) {
|
|
@@ -448,7 +449,7 @@ export default class AccountSecurityController {
|
|
|
448
449
|
await cfg.mail.onEmailChangeNotice({ email: account.email, newEmail });
|
|
449
450
|
}
|
|
450
451
|
else {
|
|
451
|
-
await sendEmailChangeNoticeEmail(ctx, {
|
|
452
|
+
await sendEmailChangeNoticeEmail(ctx, cfg, {
|
|
452
453
|
email: account.email,
|
|
453
454
|
newEmail,
|
|
454
455
|
});
|
|
@@ -530,7 +531,7 @@ export default class AccountSecurityController {
|
|
|
530
531
|
.catch(() => { });
|
|
531
532
|
}
|
|
532
533
|
else {
|
|
533
|
-
await sendEmailChangedCompletedEmail(ctx, {
|
|
534
|
+
await sendEmailChangedCompletedEmail(ctx, cfg, {
|
|
534
535
|
oldEmail: result.oldEmail,
|
|
535
536
|
newEmail: result.newEmail,
|
|
536
537
|
});
|
|
@@ -542,7 +543,7 @@ export default class AccountSecurityController {
|
|
|
542
543
|
ip: ctx.request.ip?.() ?? null,
|
|
543
544
|
timestamp: new Date().toISOString(),
|
|
544
545
|
metadata: { oldEmail: result.oldEmail, newEmail: result.newEmail },
|
|
545
|
-
}, cfg.mail, cfg.audit);
|
|
546
|
+
}, cfg.mail, cfg.audit, cfg);
|
|
546
547
|
}
|
|
547
548
|
return render(ctx, 'account/email-confirmed', { ok: result.ok });
|
|
548
549
|
}
|
|
@@ -6,28 +6,35 @@ import { brandFor, isFirstParty } from '../branding.js';
|
|
|
6
6
|
import { sendMagicLinkEmail } from '../default_mailer.js';
|
|
7
7
|
import { sendOtpUnlockEmail } from '../default_mailer.js';
|
|
8
8
|
import { translate } from '../i18n.js';
|
|
9
|
-
import { attemptPasswordLogin, isEmailUnverifiedBlock } from '../login_attempt.js';
|
|
9
|
+
import { assertLoginAllowed, attemptPasswordLogin, isEmailUnverifiedBlock, } from '../login_attempt.js';
|
|
10
10
|
import { magicChannelProp, normalizeLoginChannel } from '../login_channel.js';
|
|
11
11
|
import { notifyLoginSuccess } from '../login_notify.js';
|
|
12
|
+
import { authkitOrigin } from '../origin.js';
|
|
12
13
|
import { createOtpLockout, generateOtpUnlockToken, rawToDbOtpUnlockToken, resolveEffectiveOtpLockout, } from '../otp_lockout.js';
|
|
13
|
-
import {
|
|
14
|
+
import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
14
15
|
import { resolveEffectiveAuthMethods, resolveEffectiveMaintenanceMode, resolveEffectiveRegistration, resolveEffectiveSessionPolicy, } from '../runtime_toggles.js';
|
|
15
16
|
import { TRUSTED_DEVICE_COOKIE, buildTrustedDevicePayload, isTrustedDeviceValid, } from '../trusted_device.js';
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
18
|
+
* Chave i18n do erro de status de conta (disabled/expired) compartilhada pelos
|
|
19
|
+
* fluxos passwordless (magic link, OTP, passkey).
|
|
20
|
+
*
|
|
21
|
+
* O braço `password_expired` é HOJE inalcançável por estes três: eles passam
|
|
22
|
+
* `passwordless: true` ao gate, que pula a checagem de senha vencida (plan 012 —
|
|
23
|
+
* uma conta sem senha ficava trancada, porque estes fluxos não têm o passo de
|
|
24
|
+
* troca obrigatória para onde mandá-la). Ele permanece aqui porque
|
|
25
|
+
* {@link AccountStatusReason} é exaustivo e um chamador futuro pode não passar a
|
|
26
|
+
* flag; reaproveita o texto do passo de troca (`login.password_expired_intro`)
|
|
27
|
+
* em vez de inventar uma chave nova.
|
|
22
28
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
function accountStatusErrorKey(reason) {
|
|
30
|
+
switch (reason) {
|
|
31
|
+
case 'disabled':
|
|
32
|
+
return 'errors.account_disabled';
|
|
33
|
+
case 'account_expired':
|
|
34
|
+
return 'errors.account_expired';
|
|
35
|
+
case 'password_expired':
|
|
36
|
+
return 'login.password_expired_intro';
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
const SESSION_KEY = 'authkit_login_email';
|
|
33
40
|
/** accountId aguardando o 2º fator depois da senha verificada. */
|
|
@@ -51,7 +58,7 @@ export default class AuthInteractionController {
|
|
|
51
58
|
* maintenance) para evitar uma segunda resolução; ausente, resolve sob demanda.
|
|
52
59
|
*/
|
|
53
60
|
async #loginMethods(ctx, cfg, runtimeSettings) {
|
|
54
|
-
const settings = runtimeSettings ?? (await
|
|
61
|
+
const settings = runtimeSettings ?? (await resolveRuntimeSettingsOrNoop(ctx));
|
|
55
62
|
const magicLinkCapableConfig = cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore);
|
|
56
63
|
const authMethods = await resolveEffectiveAuthMethods(settings, {
|
|
57
64
|
configuredSocialProviders: cfg.social?.providers ?? [],
|
|
@@ -75,7 +82,7 @@ export default class AuthInteractionController {
|
|
|
75
82
|
// Maintenance mode: verifica se é uma conta admin antes de bloquear.
|
|
76
83
|
// Contas admin continuam podendo logar para que o operador possa desligar a
|
|
77
84
|
// manutenção via console. Contas comuns veem a tela de manutenção.
|
|
78
|
-
const runtimeSettingsForMaintenance = await
|
|
85
|
+
const runtimeSettingsForMaintenance = await resolveRuntimeSettingsOrNoop(ctx);
|
|
79
86
|
const maintenance = await resolveEffectiveMaintenanceMode(runtimeSettingsForMaintenance);
|
|
80
87
|
if (maintenance.enabled) {
|
|
81
88
|
// Verifica se já há uma conta admin na sessão (já autenticado) OU
|
|
@@ -145,7 +152,7 @@ export default class AuthInteractionController {
|
|
|
145
152
|
const passkeyFirstCapable = cfg.passwordless.passkeyFirst && !!acc && (await this.hasPasskeys(cfg, acc.id));
|
|
146
153
|
const passkeyFirstAvailable = authMethods.passkey && passkeyFirstCapable;
|
|
147
154
|
// Effective bot protection: may be overridden at runtime via auth_settings.
|
|
148
|
-
const effectiveBot = await resolveEffectiveBotProtection(cfg.botProtection, await
|
|
155
|
+
const effectiveBot = await resolveEffectiveBotProtection(cfg.botProtection, await resolveRuntimeSettingsOrNoop(ctx));
|
|
149
156
|
// Session policy: resolve para exibir o checkbox "manter conectado".
|
|
150
157
|
const sessionPolicyForShow = await resolveEffectiveSessionPolicy(runtimeSettingsForMaintenance, cfg.ttl?.session ? Math.ceil(cfg.ttl.session / 3600) : undefined);
|
|
151
158
|
return render(ctx, 'login', {
|
|
@@ -191,7 +198,7 @@ export default class AuthInteractionController {
|
|
|
191
198
|
const ip = ctx.request.ip?.() ?? null;
|
|
192
199
|
const clientId = details.params.client_id ?? null;
|
|
193
200
|
// Resolve runtime settings (reutilizado por bot + maintenance + verifiedEmail + session_policy).
|
|
194
|
-
const runtimeSettings = await
|
|
201
|
+
const runtimeSettings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
195
202
|
// Maintenance mode: verifica se a conta que tenta logar é admin.
|
|
196
203
|
// Admins CONTINUAM podendo logar (senão o operador se tranca fora).
|
|
197
204
|
// A verificação de role acontece APÓS as credenciais (senha correta primeiro).
|
|
@@ -411,7 +418,7 @@ export default class AuthInteractionController {
|
|
|
411
418
|
}
|
|
412
419
|
const { code, recoveryCode } = ctx.request.only(['code', 'recoveryCode']);
|
|
413
420
|
// Resolve OTP lockout settings (fail-safe).
|
|
414
|
-
const runtimeForOtp = await
|
|
421
|
+
const runtimeForOtp = await resolveRuntimeSettingsOrNoop(ctx);
|
|
415
422
|
const otpLockoutCfg = await resolveEffectiveOtpLockout(runtimeForOtp);
|
|
416
423
|
const otpLockout = createOtpLockout(otpLockoutCfg, ctx.logger);
|
|
417
424
|
// Verifica se o fator OTP está travado ANTES de tentar verificar.
|
|
@@ -572,7 +579,7 @@ export default class AuthInteractionController {
|
|
|
572
579
|
row.passwordResetExpiresAt = DateTime.now().plus({ hours: unlockTtlHours });
|
|
573
580
|
await row.save();
|
|
574
581
|
}
|
|
575
|
-
const origin =
|
|
582
|
+
const origin = authkitOrigin(cfg);
|
|
576
583
|
const unlockUrl = `${origin}/auth/otp-unlock/${encodeURIComponent(raw)}`;
|
|
577
584
|
if (cfg.mail?.onOtpUnlock) {
|
|
578
585
|
await cfg.mail.onOtpUnlock({ email: account.email, unlockUrl, token: raw }).catch(() => { });
|
|
@@ -655,7 +662,7 @@ export default class AuthInteractionController {
|
|
|
655
662
|
clientId,
|
|
656
663
|
});
|
|
657
664
|
}
|
|
658
|
-
const origin =
|
|
665
|
+
const origin = authkitOrigin(cfg);
|
|
659
666
|
const magicUrl = `${origin}/auth/interaction/${uid}/magic?token=${encodeURIComponent(issued.token)}`;
|
|
660
667
|
if (cfg.mail?.onMagicLink) {
|
|
661
668
|
await cfg.mail.onMagicLink({ email, magicUrl, token: issued.token, code, channel });
|
|
@@ -710,7 +717,7 @@ export default class AuthInteractionController {
|
|
|
710
717
|
}
|
|
711
718
|
// E-mail não verificado (LGPD/compliance): mesmo com o link válido, não
|
|
712
719
|
// materializa a sessão se a política exige verificação. Volta ao login com erro.
|
|
713
|
-
const magicLinkRuntimeSettings = await
|
|
720
|
+
const magicLinkRuntimeSettings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
714
721
|
if (await isEmailUnverifiedBlock(cfg, acc.id, magicLinkRuntimeSettings)) {
|
|
715
722
|
await cfg.audit?.record({
|
|
716
723
|
type: 'login.failure',
|
|
@@ -732,6 +739,39 @@ export default class AuthInteractionController {
|
|
|
732
739
|
error: translate(cfg.messages, 'errors.email_unverified'),
|
|
733
740
|
});
|
|
734
741
|
}
|
|
742
|
+
// Status da conta (disabled/expired): mesmo gate de `attemptPasswordLogin`,
|
|
743
|
+
// agora aplicado ao magic link (o botão "desabilitar" no admin console não
|
|
744
|
+
// tinha efeito nenhum aqui antes desta checagem). Emite o MESMO evento
|
|
745
|
+
// `login.failure`/`account.expired_login_blocked` que o fluxo de senha
|
|
746
|
+
// (auditoria uniforme entre fluxos).
|
|
747
|
+
//
|
|
748
|
+
// `passwordless: true` — este fluxo não usa senha e não tem como levar o
|
|
749
|
+
// usuário à troca obrigatória: o único caminho até `step: 'password_expired'`
|
|
750
|
+
// passa por `attemptPasswordLogin`, que exige a senha CORRETA antes de
|
|
751
|
+
// classificar a expiração. Uma conta que nunca definiu senha
|
|
752
|
+
// (`password_changed_at` NULL, que `isPasswordExpired` trata como vencida)
|
|
753
|
+
// só poderia pedir outro magic link e cair no mesmo bloqueio. Ver
|
|
754
|
+
// `LoginAllowedInput.passwordless`.
|
|
755
|
+
const magicLinkStatusGate = await assertLoginAllowed(cfg, acc.id, {
|
|
756
|
+
email: acc.email,
|
|
757
|
+
ip,
|
|
758
|
+
clientId,
|
|
759
|
+
settings: magicLinkRuntimeSettings,
|
|
760
|
+
passwordless: true,
|
|
761
|
+
});
|
|
762
|
+
if (!magicLinkStatusGate.allowed) {
|
|
763
|
+
const render = cfg.render;
|
|
764
|
+
return render(ctx, 'login', {
|
|
765
|
+
...(await this.#loginMethods(ctx, cfg)),
|
|
766
|
+
uid,
|
|
767
|
+
csrfToken: ctx.request.csrfToken,
|
|
768
|
+
step: 'password',
|
|
769
|
+
email: acc.email,
|
|
770
|
+
account: null,
|
|
771
|
+
brand: brandFor(cfg.branding, clientId ?? undefined, undefined),
|
|
772
|
+
error: translate(cfg.messages, accountStatusErrorKey(magicLinkStatusGate.reason)),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
735
775
|
await notifyLoginSuccess(ctx, cfg, {
|
|
736
776
|
accountId: acc.id,
|
|
737
777
|
email: acc.email,
|
|
@@ -791,7 +831,7 @@ export default class AuthInteractionController {
|
|
|
791
831
|
if (result.status === 'ok') {
|
|
792
832
|
// E-mail não verificado (LGPD): mesmo com código válido, não materializa a
|
|
793
833
|
// sessão se a política exige verificação. Espelha o magicLinkConsume.
|
|
794
|
-
const runtimeSettings = await
|
|
834
|
+
const runtimeSettings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
795
835
|
if (await isEmailUnverifiedBlock(cfg, result.account.id, runtimeSettings)) {
|
|
796
836
|
await cfg.audit?.record({
|
|
797
837
|
type: 'login.failure',
|
|
@@ -812,6 +852,32 @@ export default class AuthInteractionController {
|
|
|
812
852
|
error: translate(cfg.messages, 'errors.email_unverified'),
|
|
813
853
|
});
|
|
814
854
|
}
|
|
855
|
+
// Status da conta (disabled/expired): mesmo gate de `attemptPasswordLogin`,
|
|
856
|
+
// agora aplicado ao login por OTP. Emite o MESMO evento de auditoria que o
|
|
857
|
+
// fluxo de senha (uniforme entre fluxos).
|
|
858
|
+
//
|
|
859
|
+
// `passwordless: true` — mesmo raciocínio do magic link (o OTP é o outro
|
|
860
|
+
// metade do mesmo e-mail): fluxo sem senha e sem passo de troca
|
|
861
|
+
// obrigatória alcançável. Ver `LoginAllowedInput.passwordless`.
|
|
862
|
+
const otpStatusGate = await assertLoginAllowed(cfg, result.account.id, {
|
|
863
|
+
email: result.account.email,
|
|
864
|
+
ip,
|
|
865
|
+
clientId,
|
|
866
|
+
settings: runtimeSettings,
|
|
867
|
+
passwordless: true,
|
|
868
|
+
});
|
|
869
|
+
if (!otpStatusGate.allowed) {
|
|
870
|
+
return render(ctx, 'login', {
|
|
871
|
+
...(await this.#loginMethods(ctx, cfg)),
|
|
872
|
+
uid,
|
|
873
|
+
csrfToken: ctx.request.csrfToken,
|
|
874
|
+
step: 'password',
|
|
875
|
+
email: result.account.email,
|
|
876
|
+
account: null,
|
|
877
|
+
brand,
|
|
878
|
+
error: translate(cfg.messages, accountStatusErrorKey(otpStatusGate.reason)),
|
|
879
|
+
});
|
|
880
|
+
}
|
|
815
881
|
await cfg.audit?.record({
|
|
816
882
|
type: 'login.otp_verified',
|
|
817
883
|
accountId: result.account.id,
|
|
@@ -988,7 +1054,7 @@ export default class AuthInteractionController {
|
|
|
988
1054
|
// E-mail não verificado (LGPD/compliance): bloqueia mesmo com a passkey válida.
|
|
989
1055
|
// Relevante sobretudo no passkey-first (a etapa de senha — que também checa —
|
|
990
1056
|
// não rodou). Volta à tela de desafio com o erro.
|
|
991
|
-
const passkeyRuntimeSettings = await
|
|
1057
|
+
const passkeyRuntimeSettings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
992
1058
|
if (await isEmailUnverifiedBlock(cfg, accountId, passkeyRuntimeSettings)) {
|
|
993
1059
|
await cfg.audit?.record({
|
|
994
1060
|
type: 'login.failure',
|
|
@@ -1007,6 +1073,36 @@ export default class AuthInteractionController {
|
|
|
1007
1073
|
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1008
1074
|
});
|
|
1009
1075
|
}
|
|
1076
|
+
// Status da conta (disabled/expired): mesmo gate de `attemptPasswordLogin`,
|
|
1077
|
+
// agora aplicado ao passkey (cobre sobretudo o passkey-first, que nunca passa
|
|
1078
|
+
// pelo fluxo de senha). Emite o MESMO evento de auditoria que os outros fluxos.
|
|
1079
|
+
//
|
|
1080
|
+
// `passwordless: true` — no passkey-first é o dead-end mais duro dos três:
|
|
1081
|
+
// a view de erro é a `mfa-challenge`, que só oferece o desafio da passkey
|
|
1082
|
+
// (nem campo de senha nem os outros métodos de login), então uma conta sem
|
|
1083
|
+
// senha ficaria trancada sem NENHUMA ação possível. No modo MFA (pós-senha)
|
|
1084
|
+
// a flag é inócua: `attemptPasswordLogin` já rodou a checagem de senha
|
|
1085
|
+
// vencida e desviou para `step: 'password_expired'` antes de chegar aqui.
|
|
1086
|
+
// Ver `LoginAllowedInput.passwordless`.
|
|
1087
|
+
const passkeyAccount = await cfg.accountStore.findById(accountId);
|
|
1088
|
+
const passkeyStatusGate = await assertLoginAllowed(cfg, accountId, {
|
|
1089
|
+
email: passkeyAccount?.email ?? '',
|
|
1090
|
+
ip,
|
|
1091
|
+
clientId,
|
|
1092
|
+
settings: passkeyRuntimeSettings,
|
|
1093
|
+
passwordless: true,
|
|
1094
|
+
});
|
|
1095
|
+
if (!passkeyStatusGate.allowed) {
|
|
1096
|
+
return render(ctx, 'mfa-challenge', {
|
|
1097
|
+
uid: ctx.request.param('uid'),
|
|
1098
|
+
csrfToken: ctx.request.csrfToken,
|
|
1099
|
+
error: translate(cfg.messages, accountStatusErrorKey(passkeyStatusGate.reason)),
|
|
1100
|
+
brand,
|
|
1101
|
+
passkeyAvailable: await this.hasPasskeys(cfg, accountId),
|
|
1102
|
+
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
1103
|
+
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1010
1106
|
// Passkey OK: opcionalmente confia neste dispositivo (checkbox no challenge).
|
|
1011
1107
|
await this.maybeTrustDevice(ctx, cfg, accountId);
|
|
1012
1108
|
ctx.session.forget(MFA_PENDING_KEY);
|
|
@@ -1177,7 +1273,7 @@ export default class AuthInteractionController {
|
|
|
1177
1273
|
return render(ctx, 'otp-unlock', { ok: false, expired: true });
|
|
1178
1274
|
}
|
|
1179
1275
|
// Token válido: zera o lock OTP + limpa o token do DB.
|
|
1180
|
-
const runtimeForOtp = await
|
|
1276
|
+
const runtimeForOtp = await resolveRuntimeSettingsOrNoop(ctx);
|
|
1181
1277
|
const otpLockoutCfg = await resolveEffectiveOtpLockout(runtimeForOtp);
|
|
1182
1278
|
const otpLockout = createOtpLockout(otpLockoutCfg, ctx.logger);
|
|
1183
1279
|
await otpLockout.unlock(row.id);
|
|
@@ -6,6 +6,7 @@ import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protec
|
|
|
6
6
|
import { brandFor } from '../branding.js';
|
|
7
7
|
import { sendEmailVerificationEmail, sendMagicLinkEmail, sendPasswordResetEmail, } from '../default_mailer.js';
|
|
8
8
|
import { translate } from '../i18n.js';
|
|
9
|
+
import { authkitOrigin } from '../origin.js';
|
|
9
10
|
import { RuntimeSettings, resolveRuntimeSettings } from '../runtime_settings.js';
|
|
10
11
|
import { resolveEffectiveAuthMethods, resolveEffectiveMaintenanceMode, resolveEffectiveRegistration, } from '../runtime_toggles.js';
|
|
11
12
|
import { dispatchSecurityNotice } from '../security_notice_service.js';
|
|
@@ -187,7 +188,7 @@ export default class AuthRegistrationController {
|
|
|
187
188
|
email: data.email,
|
|
188
189
|
ip: ctx.request.ip?.() ?? null,
|
|
189
190
|
});
|
|
190
|
-
const origin =
|
|
191
|
+
const origin = authkitOrigin(cfg);
|
|
191
192
|
const verifyUrl = `${origin}/auth/verify-email?token=${issued.token}`;
|
|
192
193
|
// Hook do config tem prioridade (override); senão usa o mailer default do host.
|
|
193
194
|
if (cfg.mail?.onEmailVerification) {
|
|
@@ -234,7 +235,7 @@ export default class AuthRegistrationController {
|
|
|
234
235
|
// Emite + envia o magic link (mesma construção do login por magic link).
|
|
235
236
|
const issued = await accountStore.issueMagicLinkToken(data.email);
|
|
236
237
|
if (issued) {
|
|
237
|
-
const origin =
|
|
238
|
+
const origin = authkitOrigin(cfg);
|
|
238
239
|
const magicUrl = `${origin}/auth/interaction/${uid}/magic?token=${encodeURIComponent(issued.token)}`;
|
|
239
240
|
if (cfg.mail?.onMagicLink) {
|
|
240
241
|
await cfg.mail.onMagicLink({ email: data.email, magicUrl, token: issued.token });
|
|
@@ -328,7 +329,7 @@ export default class AuthRegistrationController {
|
|
|
328
329
|
email,
|
|
329
330
|
ip: ctx.request.ip?.() ?? null,
|
|
330
331
|
});
|
|
331
|
-
const origin =
|
|
332
|
+
const origin = authkitOrigin(cfg);
|
|
332
333
|
const url = `${origin}/auth/reset-password?token=${result.token}`;
|
|
333
334
|
// Hook do config tem prioridade (override); senão usa o mailer default do host.
|
|
334
335
|
if (cfg.mail?.onPasswordReset) {
|
|
@@ -430,7 +431,7 @@ export default class AuthRegistrationController {
|
|
|
430
431
|
kind: 'password_changed',
|
|
431
432
|
ip: ctx.request.ip?.() ?? null,
|
|
432
433
|
timestamp: new Date().toISOString(),
|
|
433
|
-
}, cfg.mail, cfg.audit);
|
|
434
|
+
}, cfg.mail, cfg.audit, cfg);
|
|
434
435
|
}
|
|
435
436
|
return render(ctx, 'reset', {
|
|
436
437
|
token: '',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { supportsProviderIdentity } from '../../accounts/account_store.js';
|
|
4
|
+
import { assertLoginAllowed } from '../login_attempt.js';
|
|
5
|
+
import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
4
6
|
const UID_SESSION_KEY = 'authkit_social_uid';
|
|
5
7
|
/**
|
|
6
8
|
* `AllyService.use()` é tipado contra a interface `SocialProviders`, que só é
|
|
@@ -81,6 +83,41 @@ export default class AuthSocialController {
|
|
|
81
83
|
});
|
|
82
84
|
user = created;
|
|
83
85
|
}
|
|
86
|
+
// Status da conta: mesmo gate compartilhado dos demais fluxos de login. O
|
|
87
|
+
// callback já voltou do provider OAuth — não há passo de formulário para
|
|
88
|
+
// re-renderizar um erro, então seguimos a convenção deste controller e
|
|
89
|
+
// redirecionamos de volta ao login.
|
|
90
|
+
//
|
|
91
|
+
// O QUE ESTE CAMINHO APLICA:
|
|
92
|
+
// - `disabled` → recusa (evento `login.failure` com `reason: 'disabled'`).
|
|
93
|
+
// - `account_expiration` (inatividade) → recusa (evento
|
|
94
|
+
// `account.expired_login_blocked`). Isto exige `settings`: sem eles,
|
|
95
|
+
// `assertAccountNotExpired` deixa AMBAS as checagens de expiração como
|
|
96
|
+
// no-op, e era exatamente esse o bug — uma política de controle de acesso
|
|
97
|
+
// valendo em todo lugar menos no "Continue with Google".
|
|
98
|
+
//
|
|
99
|
+
// O QUE ESTE CAMINHO **NÃO** APLICA (`passwordless: true`):
|
|
100
|
+
// - `password_expiration`. Este é um fluxo SEM senha: a conta pode nunca ter
|
|
101
|
+
// definido uma (login social puro → `password_changed_at` NULL, que
|
|
102
|
+
// `isPasswordExpired` trata como "vencida"), e este controller não tem um
|
|
103
|
+
// passo de troca obrigatória para onde mandar o usuário — só sabe
|
|
104
|
+
// redirecionar de volta ao login. Aplicar a checagem aqui trancaria
|
|
105
|
+
// permanentemente todo usuário social-only assim que o operador ligasse a
|
|
106
|
+
// política, devolvendo-o a uma tela sem senha nenhuma que pudesse trocar.
|
|
107
|
+
//
|
|
108
|
+
// `resolveRuntimeSettingsOrNoop` degrada para um RuntimeSettings no-op quando
|
|
109
|
+
// não há tabela de settings — host sem a migração continua logando por social.
|
|
110
|
+
const settings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
111
|
+
const statusGate = await assertLoginAllowed(cfg, user.id, {
|
|
112
|
+
email: user.email,
|
|
113
|
+
ip: ctx.request.ip?.() ?? null,
|
|
114
|
+
settings,
|
|
115
|
+
passwordless: true,
|
|
116
|
+
});
|
|
117
|
+
if (!statusGate.allowed) {
|
|
118
|
+
ctx.session.forget(UID_SESSION_KEY);
|
|
119
|
+
return ctx.response.redirect(backToLogin);
|
|
120
|
+
}
|
|
84
121
|
ctx.session.forget(UID_SESSION_KEY);
|
|
85
122
|
// Conclui a interaction OIDC para este usuário (escreve o 303 de volta ao authorize).
|
|
86
123
|
// O cookie de interaction (path '/') sobrevive ao round-trip do provider OAuth.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { ResolvedServerConfig } from '../define_config.js';
|
|
2
3
|
/**
|
|
3
4
|
* Envio de e-mail default do host-kit, usando o mailer `default` do host via
|
|
4
5
|
* `@adonisjs/mail`. Permite que o dev não escreva nenhum hook: por padrão os
|
|
@@ -40,7 +41,7 @@ export declare function sendEmailChangeConfirmationEmail(ctx: HttpContext, data:
|
|
|
40
41
|
* Envia o e-mail de alerta de NOVO acesso à conta (login de um IP novo).
|
|
41
42
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
42
43
|
*/
|
|
43
|
-
export declare function sendNewLoginEmail(ctx: HttpContext, data: {
|
|
44
|
+
export declare function sendNewLoginEmail(ctx: HttpContext, cfg: ResolvedServerConfig, data: {
|
|
44
45
|
email: string;
|
|
45
46
|
ip: string;
|
|
46
47
|
when: string;
|
|
@@ -49,7 +50,7 @@ export declare function sendNewLoginEmail(ctx: HttpContext, data: {
|
|
|
49
50
|
* Envia o e-mail de alerta de NOVO DISPOSITIVO (login sem cookie de dispositivo
|
|
50
51
|
* confiável). Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
51
52
|
*/
|
|
52
|
-
export declare function sendNewDeviceLoginEmail(ctx: HttpContext, data: {
|
|
53
|
+
export declare function sendNewDeviceLoginEmail(ctx: HttpContext, cfg: ResolvedServerConfig, data: {
|
|
53
54
|
email: string;
|
|
54
55
|
ip?: string | null;
|
|
55
56
|
userAgent?: string | null;
|
|
@@ -65,12 +66,42 @@ export declare function sendMagicLinkEmail(ctx: HttpContext, data: {
|
|
|
65
66
|
code?: string;
|
|
66
67
|
channel?: 'code' | 'link';
|
|
67
68
|
}): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
|
|
71
|
+
*
|
|
72
|
+
* POR QUE EXISTE. `sudoMethods.magicLink()` dependia EXCLUSIVAMENTE do hook
|
|
73
|
+
* `mail.onSudoLink`, e sem ele o método se declarava indisponível. Num host
|
|
74
|
+
* passwordless isso fechava o deadlock: sem senha, sem passkey e sem hook, a
|
|
75
|
+
* tela `/account/confirm` não tinha um único método satisfazível — e o usuário
|
|
76
|
+
* ficava trancado fora de exportar/excluir os próprios dados, do MFA, dos PATs
|
|
77
|
+
* e da troca de e-mail, inclusive do cadastro de passkey que destravaria tudo.
|
|
78
|
+
*
|
|
79
|
+
* A postura aqui é a MESMA de todos os outros e-mails da lib (reset de senha,
|
|
80
|
+
* verificação, magic link de login, avisos de segurança): o hook do host, quando
|
|
81
|
+
* existe, tem prioridade; sem hook, o próprio host-kit envia pelo mailer default
|
|
82
|
+
* (`@adonisjs/mail`) com branding e i18n; sem mailer, loga o link (dev).
|
|
83
|
+
*
|
|
84
|
+
* O TOKEN continua sendo o de sudo — emitido e verificado em
|
|
85
|
+
* `sudo/methods/magic_link.ts`, hasheado na sessão que pediu, de uso único,
|
|
86
|
+
* 5 min, vinculado ao `accountId` emissor. Isto aqui é só ENTREGA; nada é
|
|
87
|
+
* compartilhado com o token de login.
|
|
88
|
+
*
|
|
89
|
+
* @returns `true` quando o e-mail foi entregue ao mailer (ou logado no fallback
|
|
90
|
+
* de dev) e `false` quando o envio falhou de verdade. O chamador PRECISA disso:
|
|
91
|
+
* ao contrário dos outros e-mails da lib, um link de sudo que não saiu tem de
|
|
92
|
+
* apagar o pendente da sessão e recusar — flashar "link enviado" deixaria o
|
|
93
|
+
* usuário esperando um e-mail que nunca vem, na tela que já é o gargalo dele.
|
|
94
|
+
*/
|
|
95
|
+
export declare function sendSudoLinkEmail(ctx: HttpContext, data: {
|
|
96
|
+
email: string;
|
|
97
|
+
sudoUrl: string;
|
|
98
|
+
}): Promise<boolean>;
|
|
68
99
|
/**
|
|
69
100
|
* Envia o e-mail de aviso de segurança ao e-mail ATUAL quando uma troca de
|
|
70
101
|
* e-mail é solicitada ("Alguém pediu troca para X — se não foi você...").
|
|
71
102
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
72
103
|
*/
|
|
73
|
-
export declare function sendEmailChangeNoticeEmail(ctx: HttpContext, data: {
|
|
104
|
+
export declare function sendEmailChangeNoticeEmail(ctx: HttpContext, cfg: ResolvedServerConfig, data: {
|
|
74
105
|
email: string;
|
|
75
106
|
newEmail: string;
|
|
76
107
|
}): Promise<void>;
|
|
@@ -79,7 +110,7 @@ export declare function sendEmailChangeNoticeEmail(ctx: HttpContext, data: {
|
|
|
79
110
|
* troca foi concluída ("Seu e-mail foi alterado de A para B — se não foi você...").
|
|
80
111
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
81
112
|
*/
|
|
82
|
-
export declare function sendEmailChangedCompletedEmail(ctx: HttpContext, data: {
|
|
113
|
+
export declare function sendEmailChangedCompletedEmail(ctx: HttpContext, cfg: ResolvedServerConfig, data: {
|
|
83
114
|
oldEmail: string;
|
|
84
115
|
newEmail: string;
|
|
85
116
|
}): Promise<void>;
|
|
@@ -88,7 +119,7 @@ export declare function sendEmailChangedCompletedEmail(ctx: HttpContext, data: {
|
|
|
88
119
|
* MFA habilitado/desabilitado, passkey adicionada/removida, e-mail alterado).
|
|
89
120
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
90
121
|
*/
|
|
91
|
-
export declare function sendSecurityNoticeEmail(ctx: HttpContext, data: {
|
|
122
|
+
export declare function sendSecurityNoticeEmail(ctx: HttpContext, cfg: ResolvedServerConfig, data: {
|
|
92
123
|
email: string;
|
|
93
124
|
kind: 'password_changed' | 'mfa_enabled' | 'mfa_disabled' | 'passkey_added' | 'passkey_removed' | 'email_changed';
|
|
94
125
|
timestamp: string;
|