@adonis-agora/authkit-server 0.50.0 → 0.51.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/host/views/login.edge +8 -3
- package/build/src/define_config.d.ts +7 -0
- package/build/src/host/controllers/interaction_controller.js +48 -24
- package/build/src/host/default_mailer.d.ts +1 -0
- package/build/src/host/default_mailer.js +35 -14
- package/build/src/host/email_templates.d.ts +11 -4
- package/build/src/host/email_templates.js +7 -6
- package/build/src/host/i18n.d.ts +6 -0
- package/build/src/host/i18n.js +8 -0
- package/build/src/host/login_channel.d.ts +30 -0
- package/build/src/host/login_channel.js +26 -0
- package/package.json +1 -1
|
@@ -190,14 +190,19 @@
|
|
|
190
190
|
@end
|
|
191
191
|
@end
|
|
192
192
|
|
|
193
|
-
{{-- Passwordless: confirmação de magic link enviado (anti-enumeração).
|
|
193
|
+
{{-- Passwordless: confirmação de magic link enviado (anti-enumeração).
|
|
194
|
+
`magicChannel` (choose-first) escolhe a sub-view: 'code' = só o campo de
|
|
195
|
+
código, 'link' = só o aviso de link, 'both'/ausente = ambos (histórico). --}}
|
|
194
196
|
@if(magicLinkSent)
|
|
195
|
-
|
|
197
|
+
@if(magicChannel !== 'code')
|
|
198
|
+
<p class="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">{{ t('login.magic_link_sent') }}</p>
|
|
199
|
+
@end
|
|
196
200
|
|
|
197
201
|
{{-- Login por OTP: campo de código digitável (mesmo e-mail carrega link E código). --}}
|
|
198
|
-
@if(otpEnabled)
|
|
202
|
+
@if(otpEnabled && magicChannel !== 'link')
|
|
199
203
|
<form method="POST" action="/auth/interaction/{{ uid }}/otp-verify" class="mt-4">
|
|
200
204
|
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
205
|
+
<input type="hidden" name="channel" value="code">
|
|
201
206
|
<label for="otp-code" class="block text-sm font-medium text-gray-700">{{ t('login.otp_label') }}</label>
|
|
202
207
|
<input id="otp-code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code"
|
|
203
208
|
pattern="[0-9]*" placeholder="{{ t('login.otp_placeholder') }}"
|
|
@@ -47,6 +47,13 @@ export interface MailHooks {
|
|
|
47
47
|
* fluxo só-magic-link (back-compat).
|
|
48
48
|
*/
|
|
49
49
|
code?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Canal escolhido no seletor "choose-first": `'code'` = o host deveria
|
|
52
|
+
* renderizar SÓ o código, `'link'` = SÓ o link. Ausente = ambos (histórico).
|
|
53
|
+
* Puramente de superfície — os dois tokens continuam emitidos co-locados;
|
|
54
|
+
* hosts existentes simplesmente ignoram este campo (back-compat).
|
|
55
|
+
*/
|
|
56
|
+
channel?: 'code' | 'link';
|
|
50
57
|
}) => Promise<void>;
|
|
51
58
|
/**
|
|
52
59
|
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo). Distinto de
|
|
@@ -7,6 +7,7 @@ import { sendMagicLinkEmail } from '../default_mailer.js';
|
|
|
7
7
|
import { sendOtpUnlockEmail } from '../default_mailer.js';
|
|
8
8
|
import { translate } from '../i18n.js';
|
|
9
9
|
import { attemptPasswordLogin, isEmailUnverifiedBlock } from '../login_attempt.js';
|
|
10
|
+
import { magicChannelProp, normalizeLoginChannel } from '../login_channel.js';
|
|
10
11
|
import { notifyLoginSuccess } from '../login_notify.js';
|
|
11
12
|
import { createOtpLockout, generateOtpUnlockToken, rawToDbOtpUnlockToken, resolveEffectiveOtpLockout, } from '../otp_lockout.js';
|
|
12
13
|
import { RuntimeSettings, resolveRuntimeSettings } from '../runtime_settings.js';
|
|
@@ -39,17 +40,31 @@ export default class AuthInteractionController {
|
|
|
39
40
|
* passo login. Sem isto, os re-renders (erro de senha, magic link enviado, lockout…) mandam
|
|
40
41
|
* `authMethods` undefined → a view volta ao default (senha ligada), ignorando a config.
|
|
41
42
|
* `passkeyFirst` depende da conta (resolvido no fluxo principal); aqui cobrimos senha + magic link.
|
|
43
|
+
*
|
|
44
|
+
* `otpEnabled` (disponibilidade do login por OTP: config ligada E store com a
|
|
45
|
+
* capacidade) também sai daqui — é um FATO de disponibilidade de método de login,
|
|
46
|
+
* como `magicLinkAvailable`. Assim TODO render do passo login (identifier, seletor
|
|
47
|
+
* choose-first, magicLinkSent, erro) carrega a flag POR CONSTRUÇÃO: o seletor
|
|
48
|
+
* precisa dela para oferecer a opção "código" ANTES do envio do magic link.
|
|
49
|
+
*
|
|
50
|
+
* `runtimeSettings` pode ser passado já resolvido (ex.: o `show()` já resolveu para
|
|
51
|
+
* maintenance) para evitar uma segunda resolução; ausente, resolve sob demanda.
|
|
42
52
|
*/
|
|
43
|
-
async #loginMethods(ctx, cfg) {
|
|
44
|
-
const
|
|
53
|
+
async #loginMethods(ctx, cfg, runtimeSettings) {
|
|
54
|
+
const settings = runtimeSettings ?? (await getRuntimeSettings(ctx));
|
|
45
55
|
const magicLinkCapableConfig = cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore);
|
|
46
|
-
const authMethods = await resolveEffectiveAuthMethods(
|
|
56
|
+
const authMethods = await resolveEffectiveAuthMethods(settings, {
|
|
47
57
|
configuredSocialProviders: cfg.social?.providers ?? [],
|
|
48
58
|
magicLinkCapable: magicLinkCapableConfig,
|
|
49
59
|
passkeyCapable: false,
|
|
50
60
|
configOverrides: cfg.authMethods,
|
|
51
61
|
});
|
|
52
|
-
|
|
62
|
+
const otpEnabled = cfg.login.otp.enabled && supportsOtpLogin(cfg.accountStore);
|
|
63
|
+
return {
|
|
64
|
+
authMethods,
|
|
65
|
+
magicLinkAvailable: authMethods.magicLink && magicLinkCapableConfig,
|
|
66
|
+
otpEnabled,
|
|
67
|
+
};
|
|
53
68
|
}
|
|
54
69
|
async show(ctx) {
|
|
55
70
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
@@ -101,36 +116,32 @@ export default class AuthInteractionController {
|
|
|
101
116
|
});
|
|
102
117
|
}
|
|
103
118
|
const email = ctx.session.get(SESSION_KEY);
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
passkeyCapable: false, // passkey-first depends on account — resolved in step 2
|
|
113
|
-
configOverrides: cfg.authMethods,
|
|
114
|
-
});
|
|
119
|
+
// Métodos de login efetivos (magic link, OTP, social) — account-independent
|
|
120
|
+
// neste ponto (passkey-first depende da conta, resolvido no step 2). Reusa o
|
|
121
|
+
// helper compartilhado por TODOS os renders do passo login, para que
|
|
122
|
+
// `authMethods`, `magicLinkAvailable` e `otpEnabled` saiam POR CONSTRUÇÃO
|
|
123
|
+
// (o seletor choose-first precisa de `otpEnabled` para oferecer "código").
|
|
124
|
+
// Passa o runtimeSettings já resolvido para maintenance (sem 2ª resolução).
|
|
125
|
+
const loginMethods = await this.#loginMethods(ctx, cfg, runtimeSettingsForMaintenance);
|
|
126
|
+
const authMethods = loginMethods.authMethods;
|
|
115
127
|
if (!email) {
|
|
116
128
|
// Step 1: identifier (email only).
|
|
117
129
|
// Resolve registration enabled to hide "create account" link when closed.
|
|
118
130
|
const registrationEnabled = await resolveEffectiveRegistration(cfg.registration?.enabled ?? true, runtimeSettingsForMaintenance);
|
|
119
131
|
return render(ctx, 'login', {
|
|
132
|
+
...loginMethods,
|
|
120
133
|
uid: details.uid,
|
|
121
134
|
csrfToken: ctx.request.csrfToken,
|
|
122
135
|
step: 'identifier',
|
|
123
136
|
registrationEnabled,
|
|
124
137
|
brand,
|
|
125
|
-
authMethods,
|
|
126
138
|
});
|
|
127
139
|
}
|
|
128
140
|
// Step 2: password — look up user for personalisation (enumeration-safe: always show step 2)
|
|
129
141
|
const acc = await cfg.accountStore.findByEmail(email);
|
|
130
142
|
const account = acc ? { fullName: acc.name ?? null, globalRoles: acc.globalRoles ?? [] } : null;
|
|
131
|
-
// Passwordless: magic link disponível
|
|
143
|
+
// Passwordless: magic link disponível vem do helper (`loginMethods`).
|
|
132
144
|
// Passkey-first disponível se ligado, o store suporta, a conta tem passkeys E auth_methods permite.
|
|
133
|
-
const magicLinkAvailable = authMethods.magicLink && magicLinkCapableConfig;
|
|
134
145
|
const passkeyFirstCapable = cfg.passwordless.passkeyFirst && !!acc && (await this.hasPasskeys(cfg, acc.id));
|
|
135
146
|
const passkeyFirstAvailable = authMethods.passkey && passkeyFirstCapable;
|
|
136
147
|
// Effective bot protection: may be overridden at runtime via auth_settings.
|
|
@@ -138,16 +149,15 @@ export default class AuthInteractionController {
|
|
|
138
149
|
// Session policy: resolve para exibir o checkbox "manter conectado".
|
|
139
150
|
const sessionPolicyForShow = await resolveEffectiveSessionPolicy(runtimeSettingsForMaintenance, cfg.ttl?.session ? Math.ceil(cfg.ttl.session / 3600) : undefined);
|
|
140
151
|
return render(ctx, 'login', {
|
|
152
|
+
...loginMethods,
|
|
141
153
|
uid: details.uid,
|
|
142
154
|
csrfToken: ctx.request.csrfToken,
|
|
143
155
|
step: 'password',
|
|
144
156
|
email,
|
|
145
157
|
account,
|
|
146
158
|
brand,
|
|
147
|
-
magicLinkAvailable,
|
|
148
159
|
passkeyFirstAvailable,
|
|
149
160
|
botProtection: effectiveBot?.on.includes('login') ? effectiveBot.widget : undefined,
|
|
150
|
-
authMethods,
|
|
151
161
|
rememberEnabled: sessionPolicyForShow.rememberEnabled,
|
|
152
162
|
rememberDays: sessionPolicyForShow.rememberDays,
|
|
153
163
|
});
|
|
@@ -612,6 +622,10 @@ export default class AuthInteractionController {
|
|
|
612
622
|
// Login por OTP: liga o campo de código na tela "link enviado" quando a config
|
|
613
623
|
// está ligada E o store suporta a capacidade.
|
|
614
624
|
const otpEnabled = cfg.login.otp.enabled && supportsOtpLogin(cfg.accountStore);
|
|
625
|
+
// Seletor "choose-first": o host pode POSTar `channel=code|link` para pedir que
|
|
626
|
+
// o e-mail e a tela mostrem SÓ aquele método. Ausente/ inválido = both (histórico).
|
|
627
|
+
// NÃO condiciona a emissão de token — os dois continuam saindo co-locados.
|
|
628
|
+
const channel = normalizeLoginChannel(ctx.request.input('channel'));
|
|
615
629
|
if (cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore) && email) {
|
|
616
630
|
const ip = ctx.request.ip?.() ?? null;
|
|
617
631
|
const clientId = details.params.client_id ?? null;
|
|
@@ -644,14 +658,16 @@ export default class AuthInteractionController {
|
|
|
644
658
|
const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
|
|
645
659
|
const magicUrl = `${origin}/auth/interaction/${uid}/magic?token=${encodeURIComponent(issued.token)}`;
|
|
646
660
|
if (cfg.mail?.onMagicLink) {
|
|
647
|
-
await cfg.mail.onMagicLink({ email, magicUrl, token: issued.token, code });
|
|
661
|
+
await cfg.mail.onMagicLink({ email, magicUrl, token: issued.token, code, channel });
|
|
648
662
|
}
|
|
649
663
|
else {
|
|
650
|
-
await sendMagicLinkEmail(ctx, { email, magicUrl, code });
|
|
664
|
+
await sendMagicLinkEmail(ctx, { email, magicUrl, code, channel });
|
|
651
665
|
}
|
|
652
666
|
}
|
|
653
667
|
}
|
|
654
668
|
// Resposta uniforme (não vaza existência de conta).
|
|
669
|
+
// `otpEnabled` vem do spread de `#loginMethods` (mesmo valor do `otpEnabled`
|
|
670
|
+
// local usado acima para decidir a emissão) — não re-declarado aqui.
|
|
655
671
|
return render(ctx, 'login', {
|
|
656
672
|
...(await this.#loginMethods(ctx, cfg)),
|
|
657
673
|
uid,
|
|
@@ -661,7 +677,10 @@ export default class AuthInteractionController {
|
|
|
661
677
|
account: null,
|
|
662
678
|
brand,
|
|
663
679
|
magicLinkSent: true,
|
|
664
|
-
|
|
680
|
+
// Prop da tela: qual sub-view do estado `magicLinkSent` mostrar —
|
|
681
|
+
// 'code' (só o campo de código), 'link' (só o aviso de link) ou 'both'
|
|
682
|
+
// (ambos, quando o host não escolheu canal). Back-compat: ausente = 'both'.
|
|
683
|
+
magicChannel: magicChannelProp(channel),
|
|
665
684
|
});
|
|
666
685
|
}
|
|
667
686
|
/**
|
|
@@ -748,10 +767,15 @@ export default class AuthInteractionController {
|
|
|
748
767
|
}
|
|
749
768
|
const code = String(ctx.request.input('code', '') ?? '').trim();
|
|
750
769
|
const brand = brandFor(cfg.branding, clientId ?? undefined, undefined);
|
|
770
|
+
// Mantém a sub-view do seletor no re-render de erro (o form de código pode
|
|
771
|
+
// POSTar `channel=code`). Ausente = both (histórico).
|
|
772
|
+
const channel = normalizeLoginChannel(ctx.request.input('channel'));
|
|
751
773
|
const result = await cfg.accountStore.verifyLoginCode(email, uid, code, {
|
|
752
774
|
maxAttempts: cfg.login.otp.maxAttempts,
|
|
753
775
|
});
|
|
754
776
|
// Re-render da tela "link enviado" com o campo de código + erro localizado.
|
|
777
|
+
// `otpEnabled` vem do spread de `#loginMethods` (aqui, após a guarda acima,
|
|
778
|
+
// vale `true`) — não re-declarado inline.
|
|
755
779
|
const renderOtpError = async (messageKey) => render(ctx, 'login', {
|
|
756
780
|
...(await this.#loginMethods(ctx, cfg)),
|
|
757
781
|
uid,
|
|
@@ -761,7 +785,7 @@ export default class AuthInteractionController {
|
|
|
761
785
|
account: null,
|
|
762
786
|
brand,
|
|
763
787
|
magicLinkSent: true,
|
|
764
|
-
|
|
788
|
+
magicChannel: magicChannelProp(channel),
|
|
765
789
|
otpError: translate(cfg.messages, messageKey),
|
|
766
790
|
});
|
|
767
791
|
if (result.status === 'ok') {
|
|
@@ -63,6 +63,7 @@ export declare function sendMagicLinkEmail(ctx: HttpContext, data: {
|
|
|
63
63
|
email: string;
|
|
64
64
|
magicUrl: string;
|
|
65
65
|
code?: string;
|
|
66
|
+
channel?: 'code' | 'link';
|
|
66
67
|
}): Promise<void>;
|
|
67
68
|
/**
|
|
68
69
|
* Envia o e-mail de aviso de segurança ao e-mail ATUAL quando uma troca de
|
|
@@ -249,20 +249,41 @@ export async function sendMagicLinkEmail(ctx, data) {
|
|
|
249
249
|
try {
|
|
250
250
|
const brand = resolveBrand(ctx);
|
|
251
251
|
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
252
|
+
// Login choose-first: o `channel` decide o que o e-mail SURFA (os dois tokens
|
|
253
|
+
// continuam emitidos co-locados a montante — isto é só renderização).
|
|
254
|
+
// - 'code' → e-mail SÓ com o código (sem botão/link), quando há código;
|
|
255
|
+
// - 'link' → e-mail SÓ com o link (código suprimido);
|
|
256
|
+
// - ausente → ambos (comportamento histórico — back-compat).
|
|
257
|
+
// Degradação limpa: `channel: 'code'` sem código emitido (OTP desligado) cai
|
|
258
|
+
// no e-mail de link — não dá pra mostrar um código inexistente.
|
|
259
|
+
const codeOnly = data.channel === 'code' && !!data.code;
|
|
260
|
+
const linkOnly = data.channel === 'link';
|
|
261
|
+
const content = codeOnly
|
|
262
|
+
? renderTransactionalEmail({
|
|
263
|
+
brand,
|
|
264
|
+
locale,
|
|
265
|
+
subject: translate(t, 'mail.magic_link.code_subject'),
|
|
266
|
+
heading: translate(t, 'mail.magic_link.heading'),
|
|
267
|
+
intro: translate(t, 'mail.magic_link.code_intro'),
|
|
268
|
+
footnote: translate(t, 'mail.magic_link.fallback'),
|
|
269
|
+
// Sem `ctaUrl`: e-mail sem botão nem fallback de link — só o código.
|
|
270
|
+
code: data.code,
|
|
271
|
+
codeLabel: translate(t, 'mail.magic_link.code_only_label'),
|
|
272
|
+
})
|
|
273
|
+
: renderTransactionalEmail({
|
|
274
|
+
brand,
|
|
275
|
+
locale,
|
|
276
|
+
linkFallback: translate(t, 'mail.common.link_fallback'),
|
|
277
|
+
subject: translate(t, 'mail.magic_link.subject'),
|
|
278
|
+
heading: translate(t, 'mail.magic_link.heading'),
|
|
279
|
+
intro: translate(t, 'mail.magic_link.intro'),
|
|
280
|
+
ctaLabel: translate(t, 'mail.magic_link.cta'),
|
|
281
|
+
ctaUrl: data.magicUrl,
|
|
282
|
+
footnote: translate(t, 'mail.magic_link.fallback'),
|
|
283
|
+
// Código em destaque quando presente — suprimido no canal 'link'.
|
|
284
|
+
code: linkOnly ? undefined : data.code,
|
|
285
|
+
codeLabel: translate(t, 'mail.magic_link.code_label'),
|
|
286
|
+
});
|
|
266
287
|
const sent = await sendEmail(ctx, data.email, content);
|
|
267
288
|
if (!sent) {
|
|
268
289
|
ctx.logger.info({ magicUrl: data.magicUrl, email: data.email }, 'authkit: magic link de login (dev — @adonisjs/mail ausente)');
|
|
@@ -24,10 +24,17 @@ interface EmailTemplateInput {
|
|
|
24
24
|
heading: string;
|
|
25
25
|
/** Parágrafo de introdução (texto puro, será escapado). */
|
|
26
26
|
intro: string;
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Rótulo do botão de CTA. Opcional junto de `ctaUrl`: e-mails "só código"
|
|
29
|
+
* (login por OTP com `channel: 'code'`) não têm botão nem link.
|
|
30
|
+
*/
|
|
31
|
+
ctaLabel?: string;
|
|
32
|
+
/**
|
|
33
|
+
* URL do CTA. Quando ausente, o e-mail é renderizado SEM botão e SEM o
|
|
34
|
+
* fallback de link (usado no e-mail "só código"). Todos os callers históricos
|
|
35
|
+
* passam este campo — byte-parity preservado.
|
|
36
|
+
*/
|
|
37
|
+
ctaUrl?: string;
|
|
31
38
|
/** Linha auxiliar abaixo do botão (ex.: validade do link). */
|
|
32
39
|
footnote?: string;
|
|
33
40
|
/** Texto que precede o link de fallback (i18n). Default em inglês. */
|
|
@@ -51,11 +51,13 @@ export function renderTransactionalEmail(input) {
|
|
|
51
51
|
<tr><td style="padding:32px 28px 8px;">
|
|
52
52
|
<h1 style="margin:0 0 12px;font-size:20px;line-height:1.3;color:#111827;">${esc(input.heading)}</h1>
|
|
53
53
|
<p style="margin:0 0 24px;font-size:15px;line-height:1.6;color:#374151;">${esc(input.intro)}</p>
|
|
54
|
-
${codeBlock}
|
|
55
|
-
|
|
56
|
-
</
|
|
54
|
+
${codeBlock}${input.ctaUrl
|
|
55
|
+
? `<table role="presentation" cellpadding="0" cellspacing="0"><tr><td style="border-radius:8px;background:${esc(accent)};">
|
|
56
|
+
<a href="${esc(input.ctaUrl)}" style="display:inline-block;padding:12px 24px;font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;border-radius:8px;">${esc(input.ctaLabel ?? '')}</a>
|
|
57
|
+
</td></tr></table>`
|
|
58
|
+
: ''}
|
|
57
59
|
${input.footnote ? `<p style="margin:24px 0 0;font-size:13px;line-height:1.5;color:#6b7280;">${esc(input.footnote)}</p>` : ''}
|
|
58
|
-
|
|
60
|
+
${input.ctaUrl ? `<p style="margin:24px 0 0;font-size:13px;line-height:1.5;color:#6b7280;">${esc(linkFallback)}<br><a href="${esc(input.ctaUrl)}" style="color:${esc(accent)};word-break:break-all;">${esc(input.ctaUrl)}</a></p>` : ''}
|
|
59
61
|
</td></tr>
|
|
60
62
|
<tr><td style="padding:24px 28px 28px;border-top:1px solid #f3f4f6;">
|
|
61
63
|
<p style="margin:0;font-size:12px;line-height:1.5;color:#9ca3af;">${esc(company)} ${year}</p>
|
|
@@ -70,8 +72,7 @@ ${input.footnote ? `<p style="margin:24px 0 0;font-size:13px;line-height:1.5;col
|
|
|
70
72
|
'',
|
|
71
73
|
input.intro,
|
|
72
74
|
...(input.code ? ['', `${codeLabel} ${input.code}`] : []),
|
|
73
|
-
'',
|
|
74
|
-
`${input.ctaLabel}: ${input.ctaUrl}`,
|
|
75
|
+
...(input.ctaUrl ? ['', `${input.ctaLabel}: ${input.ctaUrl}`] : []),
|
|
75
76
|
...(input.footnote ? ['', input.footnote] : []),
|
|
76
77
|
'',
|
|
77
78
|
`— ${company}`,
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -518,6 +518,9 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
518
518
|
'mail.magic_link.cta': string;
|
|
519
519
|
'mail.magic_link.fallback': string;
|
|
520
520
|
'mail.magic_link.code_label': string;
|
|
521
|
+
'mail.magic_link.code_subject': string;
|
|
522
|
+
'mail.magic_link.code_intro': string;
|
|
523
|
+
'mail.magic_link.code_only_label': string;
|
|
521
524
|
'mail.new_login.subject': string;
|
|
522
525
|
'mail.new_login.heading': string;
|
|
523
526
|
'mail.new_login.intro': string;
|
|
@@ -1219,6 +1222,9 @@ export declare const PT_BR_MESSAGES: {
|
|
|
1219
1222
|
'mail.magic_link.cta': string;
|
|
1220
1223
|
'mail.magic_link.fallback': string;
|
|
1221
1224
|
'mail.magic_link.code_label': string;
|
|
1225
|
+
'mail.magic_link.code_subject': string;
|
|
1226
|
+
'mail.magic_link.code_intro': string;
|
|
1227
|
+
'mail.magic_link.code_only_label': string;
|
|
1222
1228
|
'mail.new_login.subject': string;
|
|
1223
1229
|
'mail.new_login.heading': string;
|
|
1224
1230
|
'mail.new_login.intro': string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -558,6 +558,10 @@ export const DEFAULT_MESSAGES = {
|
|
|
558
558
|
'mail.magic_link.cta': 'Sign in',
|
|
559
559
|
'mail.magic_link.fallback': 'If you did not request this, you can ignore this email.',
|
|
560
560
|
'mail.magic_link.code_label': 'Or enter this code to sign in:',
|
|
561
|
+
// E-mail "só código" (login choose-first com channel: 'code'): sem botão/link.
|
|
562
|
+
'mail.magic_link.code_subject': 'Your login code',
|
|
563
|
+
'mail.magic_link.code_intro': 'Use the code below to sign in. It expires shortly and can be used once.',
|
|
564
|
+
'mail.magic_link.code_only_label': 'Enter this code to sign in:',
|
|
561
565
|
'mail.new_login.subject': 'New login to your account',
|
|
562
566
|
'mail.new_login.heading': 'New login detected',
|
|
563
567
|
'mail.new_login.intro': 'We detected a new login to your account.',
|
|
@@ -1335,6 +1339,10 @@ export const PT_BR_MESSAGES = {
|
|
|
1335
1339
|
'mail.magic_link.cta': 'Entrar',
|
|
1336
1340
|
'mail.magic_link.fallback': 'Se você não solicitou isso, pode ignorar este e-mail.',
|
|
1337
1341
|
'mail.magic_link.code_label': 'Ou digite este código para entrar:',
|
|
1342
|
+
// E-mail "só código" (login choose-first com channel: 'code'): sem botão/link.
|
|
1343
|
+
'mail.magic_link.code_subject': 'Seu código de login',
|
|
1344
|
+
'mail.magic_link.code_intro': 'Use o código abaixo para entrar. Ele expira em instantes e serve para um único acesso.',
|
|
1345
|
+
'mail.magic_link.code_only_label': 'Digite este código para entrar:',
|
|
1338
1346
|
'mail.new_login.subject': 'Novo login na sua conta',
|
|
1339
1347
|
'mail.new_login.heading': 'Novo login detectado',
|
|
1340
1348
|
'mail.new_login.intro': 'Detectamos um novo login na sua conta.',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canal de login escolhido no seletor "choose-first" (modelo GitHub): o usuário
|
|
3
|
+
* decide PRIMEIRO como quer entrar e só então o método é executado.
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANTE — o `channel` é puramente de SUPERFÍCIE: NÃO condiciona a emissão de
|
|
6
|
+
* token nem toca no codec `ml2:` / lockout / single-use-conjunto. A lib continua
|
|
7
|
+
* emitindo link E código co-locados (quando `login.otp.enabled`); o `channel` só
|
|
8
|
+
* decide o que o E-MAIL renderiza e qual sub-view a TELA mostra no estado
|
|
9
|
+
* `magicLinkSent`. Ausente/ inválido = comportamento histórico ("both").
|
|
10
|
+
*/
|
|
11
|
+
/** Método escolhido no seletor. `passkey` é slot documentado (ainda não emitido). */
|
|
12
|
+
export type LoginChannel = 'code' | 'link';
|
|
13
|
+
/**
|
|
14
|
+
* Valor da prop de render que a tela usa para escolher a sub-view do estado
|
|
15
|
+
* `magicLinkSent`: `'code'` = só campo de código, `'link'` = só aviso de link,
|
|
16
|
+
* `'both'` = ambos (comportamento histórico, quando o host não manda `channel`).
|
|
17
|
+
*/
|
|
18
|
+
export type MagicChannelProp = 'code' | 'link' | 'both';
|
|
19
|
+
/**
|
|
20
|
+
* Lê e valida o campo `channel` do body do POST `/magic`. Só `'code'` e `'link'`
|
|
21
|
+
* são aceitos; qualquer outro valor (ausente, vazio, lixo) vira `undefined`, que
|
|
22
|
+
* a lib trata como "both" — garantindo back-compat total com hosts que ainda
|
|
23
|
+
* POSTam sem o campo.
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeLoginChannel(raw: unknown): LoginChannel | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Mapeia o `channel` do body para a prop de render `magicChannel`. Ausente
|
|
28
|
+
* (`undefined`) → `'both'`: a tela mostra as duas sub-views, como hoje.
|
|
29
|
+
*/
|
|
30
|
+
export declare function magicChannelProp(channel: LoginChannel | undefined): MagicChannelProp;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canal de login escolhido no seletor "choose-first" (modelo GitHub): o usuário
|
|
3
|
+
* decide PRIMEIRO como quer entrar e só então o método é executado.
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANTE — o `channel` é puramente de SUPERFÍCIE: NÃO condiciona a emissão de
|
|
6
|
+
* token nem toca no codec `ml2:` / lockout / single-use-conjunto. A lib continua
|
|
7
|
+
* emitindo link E código co-locados (quando `login.otp.enabled`); o `channel` só
|
|
8
|
+
* decide o que o E-MAIL renderiza e qual sub-view a TELA mostra no estado
|
|
9
|
+
* `magicLinkSent`. Ausente/ inválido = comportamento histórico ("both").
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Lê e valida o campo `channel` do body do POST `/magic`. Só `'code'` e `'link'`
|
|
13
|
+
* são aceitos; qualquer outro valor (ausente, vazio, lixo) vira `undefined`, que
|
|
14
|
+
* a lib trata como "both" — garantindo back-compat total com hosts que ainda
|
|
15
|
+
* POSTam sem o campo.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeLoginChannel(raw) {
|
|
18
|
+
return raw === 'code' || raw === 'link' ? raw : undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Mapeia o `channel` do body para a prop de render `magicChannel`. Ausente
|
|
22
|
+
* (`undefined`) → `'both'`: a tela mostra as duas sub-views, como hoje.
|
|
23
|
+
*/
|
|
24
|
+
export function magicChannelProp(channel) {
|
|
25
|
+
return channel ?? 'both';
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.1",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|