@adonis-agora/authkit-server 1.0.0-rc.13 → 1.0.0-rc.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/build/index.d.ts +1 -1
- package/build/src/commands/expire_scan_command.d.ts +10 -8
- package/build/src/commands/expire_scan_command.js +26 -26
- package/build/src/define_config.d.ts +3 -3
- package/build/src/host/account_api/account_api_controller.js +1 -1
- package/build/src/host/account_api/account_orgs_api_controller.js +1 -1
- package/build/src/host/admin_api/admin_orgs_service.js +1 -1
- package/build/src/host/admin_api/admin_users_service.js +1 -1
- package/build/src/host/branding.d.ts +14 -1
- package/build/src/host/branding.js +30 -1
- package/build/src/host/controllers/account_orgs_controller.js +1 -1
- package/build/src/host/controllers/account_security_controller.js +1 -1
- package/build/src/host/controllers/interaction_controller.js +2 -2
- package/build/src/host/controllers/registration_controller.js +4 -4
- package/build/src/host/default_mailer.d.ts +25 -7
- package/build/src/host/default_mailer.js +175 -101
- package/build/src/host/i18n.d.ts +10 -0
- package/build/src/host/i18n.js +12 -0
- package/build/src/host/sudo/methods/magic_link.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @adonis-agora/authkit-server
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#477](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/477) [`c17adde`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/c17addeef2bbc80a1fea41ab5b6848b37fad7f24) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - The `authkit:accounts:expire-scan --warn` inactivity warning now goes through the default mailer like every other built-in email: the `mail.onAccountExpirationWarning` hook still wins, otherwise it is sent in the host locale with the host branding (new `mail.account_expiration.*` messages, en + pt-BR) instead of a hardcoded English email.
|
|
8
|
+
|
|
9
|
+
`branding` without `default` (or without `firstParty`) is now resolved once: the default brand falls back to `branding.company`, then `'AuthKit'`, the same precedence the mailer used. Screens without a known client (session expired, throttled, login/consent for an unlisted client) now show the company name and a theme accent instead of the generic fallback, and a missing `firstParty` no longer crashes the consent check. `BrandingConfigInput` makes both fields optional in `defineConfig`.
|
|
10
|
+
|
|
11
|
+
## 1.0.0-rc.14
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#476](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/476) [`b9f9119`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/b9f9119a4a030b1fb03befd73d018abd88e8bf6c) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Default emails (new login / new device alerts, password reset, verification, magic link, sudo link, OTP unlock, email change, security notices, org invitations, `sendAuthkitCodeEmail`) now always use the host's `i18n.locale` and `branding`. They used to read the config through `ctx.containerResolver.app`, which a real request's container resolver does not have, so they silently fell back to English and the "AuthKit" brand. The mailer now uses the resolved config the caller already has, or resolves the `authkit.server` binding, and logs a warning once when it cannot find the config. The header falls back to `branding.company` when `branding.default` is absent.
|
|
16
|
+
|
|
3
17
|
## 1.0.0-rc.13
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/build/index.d.ts
CHANGED
|
@@ -75,7 +75,7 @@ export { computeAdminStats } from './src/host/admin_stats_service.js';
|
|
|
75
75
|
export { authkitUserProvider } from './src/host/adonis_auth_user_provider.js';
|
|
76
76
|
export type { BotProtectionAction, BotProtectionConfigInput, BotProtectionSetting, BotProtectionVerifyInput, BotProtectionWidget, ResolvedBotProtectionConfig, } from './src/host/bot_protection.js';
|
|
77
77
|
export { botProtectionApplies, DEFAULT_BOT_TOKEN_FIELDS, extractBotToken, guardBotProtection, resolveBotProtection, resolveEffectiveBotProtection, verifyBotProtection, } from './src/host/bot_protection.js';
|
|
78
|
-
export type { BrandingConfig, ClientBrand } from './src/host/branding.js';
|
|
78
|
+
export type { BrandingConfig, BrandingConfigInput, ClientBrand } from './src/host/branding.js';
|
|
79
79
|
export { brandFor, isFirstParty, isFirstPartyClient } from './src/host/branding.js';
|
|
80
80
|
export type { ApplicationType } from './src/host/client_metadata.js';
|
|
81
81
|
export { assertClientMetadata, ClientMetadataError, redirectUriProblem, } from './src/host/client_metadata.js';
|
|
@@ -11,15 +11,20 @@
|
|
|
11
11
|
* queries ao audit, o que é compatível com volumes moderados (<= 50k contas).
|
|
12
12
|
* Para volumes maiores, o operador deve implementar um sink com agregação nativa.
|
|
13
13
|
* - `--warn`: envia e-mail de aviso ("sua conta expira em N dias — faça login") via
|
|
14
|
-
* hook `mail.onAccountExpirationWarning`
|
|
15
|
-
* audit `account.expiration_warned` — não re-avisa
|
|
16
|
-
* da janela de `warnDays` dias.
|
|
14
|
+
* hook `mail.onAccountExpirationWarning` ou, sem ele, o mailer default (i18n +
|
|
15
|
+
* branding). Deduplicado via audit `account.expiration_warned` — não re-avisa
|
|
16
|
+
* quem já foi avisado dentro da janela de `warnDays` dias.
|
|
17
17
|
* - `--dry-run`: reporta sem enviar e-mails nem auditar.
|
|
18
18
|
* - `--json`: output machine-readable.
|
|
19
19
|
*
|
|
20
20
|
* Designed pra cron do host (ex.: `0 2 * * * node ace authkit:accounts:expire-scan --warn`).
|
|
21
21
|
*/
|
|
22
22
|
import type { ApplicationService } from '@adonisjs/core/types';
|
|
23
|
+
type ScanLogger = {
|
|
24
|
+
info: (m: string) => void;
|
|
25
|
+
warn: (m: string) => void;
|
|
26
|
+
error: (m: string) => void;
|
|
27
|
+
};
|
|
23
28
|
export interface ExpireScanResult {
|
|
24
29
|
/** Contas já expiradas (bloqueadas no login). */
|
|
25
30
|
expired: Array<{
|
|
@@ -52,9 +57,6 @@ export declare function runExpireScan(app: ApplicationService, opts: {
|
|
|
52
57
|
dryRun?: boolean;
|
|
53
58
|
warn?: boolean;
|
|
54
59
|
json?: boolean;
|
|
55
|
-
logger:
|
|
56
|
-
info: (m: string) => void;
|
|
57
|
-
warn: (m: string) => void;
|
|
58
|
-
error: (m: string) => void;
|
|
59
|
-
};
|
|
60
|
+
logger: ScanLogger;
|
|
60
61
|
}): Promise<ExpireScanResult>;
|
|
62
|
+
export {};
|
|
@@ -11,22 +11,15 @@
|
|
|
11
11
|
* queries ao audit, o que é compatível com volumes moderados (<= 50k contas).
|
|
12
12
|
* Para volumes maiores, o operador deve implementar um sink com agregação nativa.
|
|
13
13
|
* - `--warn`: envia e-mail de aviso ("sua conta expira em N dias — faça login") via
|
|
14
|
-
* hook `mail.onAccountExpirationWarning`
|
|
15
|
-
* audit `account.expiration_warned` — não re-avisa
|
|
16
|
-
* da janela de `warnDays` dias.
|
|
14
|
+
* hook `mail.onAccountExpirationWarning` ou, sem ele, o mailer default (i18n +
|
|
15
|
+
* branding). Deduplicado via audit `account.expiration_warned` — não re-avisa
|
|
16
|
+
* quem já foi avisado dentro da janela de `warnDays` dias.
|
|
17
17
|
* - `--dry-run`: reporta sem enviar e-mails nem auditar.
|
|
18
18
|
* - `--json`: output machine-readable.
|
|
19
19
|
*
|
|
20
20
|
* Designed pra cron do host (ex.: `0 2 * * * node ace authkit:accounts:expire-scan --warn`).
|
|
21
21
|
*/
|
|
22
|
-
|
|
23
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
24
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
25
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
return path;
|
|
29
|
-
};
|
|
22
|
+
import { sendAccountExpirationWarningEmail } from '../host/default_mailer.js';
|
|
30
23
|
import { RuntimeSettings, settingsLocation } from '../host/runtime_settings.js';
|
|
31
24
|
import { resolveEffectiveAccountExpiration } from '../host/runtime_toggles.js';
|
|
32
25
|
import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
|
|
@@ -114,7 +107,7 @@ export async function runExpireScan(app, opts) {
|
|
|
114
107
|
}
|
|
115
108
|
else {
|
|
116
109
|
// Envia e-mail de aviso.
|
|
117
|
-
await sendWarnEmail(cfg, account, daysUntilExpiry);
|
|
110
|
+
await sendWarnEmail(cfg, account, daysUntilExpiry, logger);
|
|
118
111
|
// Audita para deduplicação.
|
|
119
112
|
await audit.record({
|
|
120
113
|
type: 'account.expiration_warned',
|
|
@@ -199,31 +192,38 @@ async function wasWarnedRecently(audit, accountId, warnDays) {
|
|
|
199
192
|
return false;
|
|
200
193
|
}
|
|
201
194
|
}
|
|
202
|
-
/**
|
|
203
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Envia o aviso de expiração iminente pelo MESMO caminho dos outros e-mails da lib: o hook
|
|
197
|
+
* `mail.onAccountExpirationWarning` do host tem prioridade; sem ele, o mailer default
|
|
198
|
+
* (`@adonisjs/mail`) com o locale e o branding do config resolvido. Best-effort: nunca
|
|
199
|
+
* propaga erros de e-mail (o scan segue para a próxima conta).
|
|
200
|
+
*/
|
|
201
|
+
async function sendWarnEmail(cfg, account, expiresInDays, logger) {
|
|
204
202
|
try {
|
|
205
203
|
if (cfg.mail?.onAccountExpirationWarning) {
|
|
206
204
|
await cfg.mail.onAccountExpirationWarning({ email: account.email, expiresInDays });
|
|
207
205
|
return;
|
|
208
206
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const mail = mailMod?.default ?? null;
|
|
213
|
-
if (!mail)
|
|
214
|
-
return; // Sem mail configurado — silêncio.
|
|
215
|
-
await mail.send((message) => {
|
|
216
|
-
message
|
|
217
|
-
.to(account.email)
|
|
218
|
-
.subject('Your account will be deactivated due to inactivity')
|
|
219
|
-
.html(`<p>Your account will be deactivated in <strong>${expiresInDays} day(s)</strong> due to inactivity. Sign in to keep your account active.</p>`)
|
|
220
|
-
.text(`Your account will be deactivated in ${expiresInDays} day(s) due to inactivity. Sign in to keep your account active.`);
|
|
207
|
+
await sendAccountExpirationWarningEmail({ logger: mailLogger(logger) }, cfg, {
|
|
208
|
+
email: account.email,
|
|
209
|
+
expiresInDays,
|
|
221
210
|
});
|
|
222
211
|
}
|
|
223
212
|
catch {
|
|
224
213
|
// Best-effort: nunca propaga erros de e-mail.
|
|
225
214
|
}
|
|
226
215
|
}
|
|
216
|
+
/** Adapta o logger do Ace (`info(msg)`) ao formato pino (`info(obj, msg)`) do mailer. */
|
|
217
|
+
function mailLogger(logger) {
|
|
218
|
+
const format = (obj, msg) => typeof msg === 'string' ? `${msg} ${JSON.stringify(obj, errorReplacer)}` : String(obj);
|
|
219
|
+
return {
|
|
220
|
+
info: (obj, msg) => logger.info(format(obj, msg)),
|
|
221
|
+
error: (obj, msg) => logger.error(format(obj, msg)),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function errorReplacer(_key, value) {
|
|
225
|
+
return value instanceof Error ? value.message : value;
|
|
226
|
+
}
|
|
227
227
|
function emptyResult(dryRun, inactiveDays, warnDays, auditSupported = false) {
|
|
228
228
|
return {
|
|
229
229
|
expired: [],
|
|
@@ -8,7 +8,7 @@ import type { AuditSink } from './audit/audit_sink.js';
|
|
|
8
8
|
import type { ConsoleAuthorize } from './console_authorize.js';
|
|
9
9
|
import { type EventsConfigInput } from './events/dispatcher.js';
|
|
10
10
|
import { type BotProtectionConfigInput, type ResolvedBotProtectionConfig } from './host/bot_protection.js';
|
|
11
|
-
import { type BrandingConfig } from './host/branding.js';
|
|
11
|
+
import { type BrandingConfig, type BrandingConfigInput } from './host/branding.js';
|
|
12
12
|
import { type PolicyRouteOption } from './host/config_locks.js';
|
|
13
13
|
import type { CustomLoginMethods } from './host/custom_login.js';
|
|
14
14
|
import type { CustomMfaMethods } from './host/custom_mfa.js';
|
|
@@ -959,8 +959,8 @@ export interface AuthServerConfigInput {
|
|
|
959
959
|
* {@link InteractionRecoveryConfigInput}.
|
|
960
960
|
*/
|
|
961
961
|
interactionRecovery?: InteractionRecoveryConfigInput;
|
|
962
|
-
/** Configuração de branding por cliente. */
|
|
963
|
-
branding?:
|
|
962
|
+
/** Configuração de branding por cliente (`default`/`firstParty` opcionais). */
|
|
963
|
+
branding?: BrandingConfigInput;
|
|
964
964
|
/**
|
|
965
965
|
* Allowlist dos clients FIRST-PARTY: quem recebe as claims de AUTORIZAÇÃO
|
|
966
966
|
* (`<globalRolesClaim>`, `org_id`, `org_slug`, `org_role`) nos tokens.
|
|
@@ -404,7 +404,7 @@ export default class AccountApiController {
|
|
|
404
404
|
});
|
|
405
405
|
}
|
|
406
406
|
else {
|
|
407
|
-
await sendEmailChangeConfirmationEmail(ctx, { email: newEmail, confirmUrl });
|
|
407
|
+
await sendEmailChangeConfirmationEmail(ctx, { email: newEmail, confirmUrl }, cfg);
|
|
408
408
|
}
|
|
409
409
|
if (account) {
|
|
410
410
|
if (cfg.mail?.onEmailChangeNotice) {
|
|
@@ -272,7 +272,7 @@ export default class AccountOrgsApiController {
|
|
|
272
272
|
if (c.cfg.mail?.onOrgInvitation)
|
|
273
273
|
await c.cfg.mail.onOrgInvitation(payload);
|
|
274
274
|
else
|
|
275
|
-
await sendOrgInvitationEmail(ctx, payload);
|
|
275
|
+
await sendOrgInvitationEmail(ctx, payload, c.cfg);
|
|
276
276
|
}
|
|
277
277
|
catch {
|
|
278
278
|
/* best-effort */
|
|
@@ -15,13 +15,26 @@ export interface BrandingConfig {
|
|
|
15
15
|
firstParty: string[];
|
|
16
16
|
audienceLabels?: Record<string, string>;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* O que o host declara em `defineConfig({ branding })`. `default` e `firstParty` são
|
|
20
|
+
* opcionais: sem `default` a marca de quem não tem client conhecido vem da `company`
|
|
21
|
+
* (ver {@link resolveDefaultBrand}); sem `firstParty`, nenhum client pula o consent.
|
|
22
|
+
*/
|
|
23
|
+
export type BrandingConfigInput = Omit<BrandingConfig, 'default' | 'firstParty'> & Partial<Pick<BrandingConfig, 'default' | 'firstParty'>>;
|
|
18
24
|
/**
|
|
19
25
|
* Resolve a config de branding: quando o host não declara `branding`, cai no
|
|
20
26
|
* {@link DEFAULT_BRANDING} acima (tema neutro, sem clients customizados) em
|
|
21
27
|
* vez de deixar `cfg.branding` como `undefined` — o que fazia a MENOR config
|
|
22
28
|
* documentada (sem branding) quebrar a primeira tela de login/consent/signup.
|
|
23
29
|
*/
|
|
24
|
-
export declare function resolveBranding(input:
|
|
30
|
+
export declare function resolveBranding(input: BrandingConfigInput | undefined): BrandingConfig;
|
|
31
|
+
/**
|
|
32
|
+
* A marca default de um host — o ÚNICO lugar que decide o nome/cor de quem não tem
|
|
33
|
+
* client conhecido (telas sem client, e-mails). Precedência do nome:
|
|
34
|
+
* `branding.default.appName` → `branding.company` → `'AuthKit'`; as cores caem no
|
|
35
|
+
* tema neutro. Tolera `branding` parcial/ausente (configs crus, fixtures).
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveDefaultBrand(branding: Partial<BrandingConfigInput> | undefined): BrandingConfig['default'];
|
|
25
38
|
export declare function isFirstParty(cfg: BrandingConfig, clientId: string | undefined): boolean;
|
|
26
39
|
/**
|
|
27
40
|
* O client é FIRST-PARTY, ou seja, recebe as claims de autorização
|
|
@@ -27,7 +27,36 @@ const DEFAULT_BRANDING = {
|
|
|
27
27
|
* documentada (sem branding) quebrar a primeira tela de login/consent/signup.
|
|
28
28
|
*/
|
|
29
29
|
export function resolveBranding(input) {
|
|
30
|
-
|
|
30
|
+
if (!input)
|
|
31
|
+
return DEFAULT_BRANDING;
|
|
32
|
+
// Hosts declaram na prática só `company` + `clients` (sem `default`, às vezes sem
|
|
33
|
+
// `firstParty`). Sem normalizar, `brandFor` devolvia uma marca SEM `appName`/`accent`
|
|
34
|
+
// para todo fluxo sem client conhecido (console de conta, recuperação, throttle) — as
|
|
35
|
+
// telas caíam no fallback genérico enquanto os e-mails já usavam a `company` — e
|
|
36
|
+
// `isFirstParty` estourava em `firstParty.includes`.
|
|
37
|
+
return {
|
|
38
|
+
...input,
|
|
39
|
+
clients: input.clients ?? {},
|
|
40
|
+
default: resolveDefaultBrand(input),
|
|
41
|
+
firstParty: input.firstParty ?? [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A marca default de um host — o ÚNICO lugar que decide o nome/cor de quem não tem
|
|
46
|
+
* client conhecido (telas sem client, e-mails). Precedência do nome:
|
|
47
|
+
* `branding.default.appName` → `branding.company` → `'AuthKit'`; as cores caem no
|
|
48
|
+
* tema neutro. Tolera `branding` parcial/ausente (configs crus, fixtures).
|
|
49
|
+
*/
|
|
50
|
+
export function resolveDefaultBrand(branding) {
|
|
51
|
+
const declared = branding?.default;
|
|
52
|
+
const neutral = DEFAULT_BRANDING.default;
|
|
53
|
+
return {
|
|
54
|
+
...declared,
|
|
55
|
+
appName: declared?.appName || branding?.company || neutral.appName,
|
|
56
|
+
accent: declared?.accent || neutral.accent,
|
|
57
|
+
accentSoft: declared?.accentSoft || declared?.accent || neutral.accentSoft,
|
|
58
|
+
tagline: declared?.tagline ?? neutral.tagline,
|
|
59
|
+
};
|
|
31
60
|
}
|
|
32
61
|
export function isFirstParty(cfg, clientId) {
|
|
33
62
|
return !!clientId && cfg.firstParty.includes(clientId);
|
|
@@ -1098,7 +1098,7 @@ export default class AuthInteractionController {
|
|
|
1098
1098
|
await cfg.mail.onOtpUnlock({ email: account.email, unlockUrl, token: raw }).catch(() => { });
|
|
1099
1099
|
}
|
|
1100
1100
|
else {
|
|
1101
|
-
await sendOtpUnlockEmail(ctx, { email: account.email, unlockUrl }).catch(() => { });
|
|
1101
|
+
await sendOtpUnlockEmail(ctx, { email: account.email, unlockUrl }, cfg).catch(() => { });
|
|
1102
1102
|
}
|
|
1103
1103
|
}
|
|
1104
1104
|
catch {
|
|
@@ -1200,7 +1200,7 @@ export default class AuthInteractionController {
|
|
|
1200
1200
|
await cfg.mail.onMagicLink({ email: to, magicUrl, token: issued.token, code, channel });
|
|
1201
1201
|
}
|
|
1202
1202
|
else {
|
|
1203
|
-
await sendMagicLinkEmail(ctx, { email: to, magicUrl, code, channel });
|
|
1203
|
+
await sendMagicLinkEmail(ctx, { email: to, magicUrl, code, channel }, cfg);
|
|
1204
1204
|
}
|
|
1205
1205
|
}
|
|
1206
1206
|
}
|
|
@@ -210,7 +210,7 @@ export default class AuthRegistrationController {
|
|
|
210
210
|
await cfg.mail.onEmailVerification({ email: data.email, verifyUrl, token: issued.token });
|
|
211
211
|
}
|
|
212
212
|
else {
|
|
213
|
-
await sendEmailVerificationEmail(ctx, { email: data.email, verifyUrl });
|
|
213
|
+
await sendEmailVerificationEmail(ctx, { email: data.email, verifyUrl }, cfg);
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
}
|
|
@@ -255,7 +255,7 @@ export default class AuthRegistrationController {
|
|
|
255
255
|
await cfg.mail.onMagicLink({ email: to, magicUrl, token: issued.token, code });
|
|
256
256
|
}
|
|
257
257
|
else {
|
|
258
|
-
await sendMagicLinkEmail(ctx, { email: to, magicUrl, code });
|
|
258
|
+
await sendMagicLinkEmail(ctx, { email: to, magicUrl, code }, cfg);
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
261
|
}
|
|
@@ -279,7 +279,7 @@ export default class AuthRegistrationController {
|
|
|
279
279
|
await cfg.mail.onMagicLink({ email: data.email, magicUrl, token, code, kind: 'signup' });
|
|
280
280
|
}
|
|
281
281
|
else {
|
|
282
|
-
await sendMagicLinkEmail(ctx, { email: data.email, magicUrl, code });
|
|
282
|
+
await sendMagicLinkEmail(ctx, { email: data.email, magicUrl, code }, cfg);
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
// Resposta uniforme: "enviamos um link" (não vaza existência da conta).
|
|
@@ -436,7 +436,7 @@ export default class AuthRegistrationController {
|
|
|
436
436
|
await cfg.mail.onPasswordReset({ email: to, resetUrl: url, token: result.token });
|
|
437
437
|
}
|
|
438
438
|
else {
|
|
439
|
-
await sendPasswordResetEmail(ctx, { email: to, resetUrl: url });
|
|
439
|
+
await sendPasswordResetEmail(ctx, { email: to, resetUrl: url }, cfg);
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
return render(ctx, 'forgot', {
|
|
@@ -21,6 +21,11 @@ type MailService = any;
|
|
|
21
21
|
* @internal
|
|
22
22
|
*/
|
|
23
23
|
export declare function __setMailLoaderForTests(fn: (() => Promise<MailService | null>) | undefined): void;
|
|
24
|
+
/**
|
|
25
|
+
* Permite rearmar o warning "config não resolvido" (usado em testes).
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export declare function __resetMailConfigWarningForTests(): void;
|
|
24
29
|
/**
|
|
25
30
|
* Envia o e-mail de redefinição de senha pelo mailer default do host.
|
|
26
31
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
@@ -28,7 +33,7 @@ export declare function __setMailLoaderForTests(fn: (() => Promise<MailService |
|
|
|
28
33
|
export declare function sendPasswordResetEmail(ctx: HttpContext, data: {
|
|
29
34
|
email: string;
|
|
30
35
|
resetUrl: string;
|
|
31
|
-
}): Promise<void>;
|
|
36
|
+
}, cfg?: ResolvedServerConfig): Promise<void>;
|
|
32
37
|
/**
|
|
33
38
|
* Envia o e-mail de confirmação de TROCA de e-mail para o NOVO endereço.
|
|
34
39
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
@@ -36,7 +41,7 @@ export declare function sendPasswordResetEmail(ctx: HttpContext, data: {
|
|
|
36
41
|
export declare function sendEmailChangeConfirmationEmail(ctx: HttpContext, data: {
|
|
37
42
|
email: string;
|
|
38
43
|
confirmUrl: string;
|
|
39
|
-
}): Promise<void>;
|
|
44
|
+
}, cfg?: ResolvedServerConfig): Promise<void>;
|
|
40
45
|
/**
|
|
41
46
|
* Envia o e-mail de alerta de NOVO acesso à conta (login de um IP novo).
|
|
42
47
|
* Best-effort: no fallback (sem mail) loga o evento; nunca lança.
|
|
@@ -65,7 +70,7 @@ export declare function sendMagicLinkEmail(ctx: HttpContext, data: {
|
|
|
65
70
|
magicUrl: string;
|
|
66
71
|
code?: string;
|
|
67
72
|
channel?: 'code' | 'link';
|
|
68
|
-
}): Promise<void>;
|
|
73
|
+
}, cfg?: ResolvedServerConfig): Promise<void>;
|
|
69
74
|
/**
|
|
70
75
|
* Envia um e-mail só com um código (sem link: quem digita é a pessoa, em outro lugar) pelo mailer
|
|
71
76
|
* default do host, com o branding e o i18n do authkit. Os textos saem do catálogo pelas chaves
|
|
@@ -81,7 +86,7 @@ export declare function sendAuthkitCodeEmail(ctx: HttpContext, data: {
|
|
|
81
86
|
/** Prefixo das chaves no catálogo (`'mail.poppy_sign_in'`). */
|
|
82
87
|
messages: string;
|
|
83
88
|
params?: Record<string, string | number>;
|
|
84
|
-
}): Promise<void>;
|
|
89
|
+
}, cfg?: ResolvedServerConfig): Promise<void>;
|
|
85
90
|
/**
|
|
86
91
|
* Envia o link de CONFIRMAÇÃO DE IDENTIDADE (sudo) pelo mailer default do host.
|
|
87
92
|
*
|
|
@@ -111,7 +116,7 @@ export declare function sendAuthkitCodeEmail(ctx: HttpContext, data: {
|
|
|
111
116
|
export declare function sendSudoLinkEmail(ctx: HttpContext, data: {
|
|
112
117
|
email: string;
|
|
113
118
|
sudoUrl: string;
|
|
114
|
-
}): Promise<boolean>;
|
|
119
|
+
}, cfg?: ResolvedServerConfig): Promise<boolean>;
|
|
115
120
|
/**
|
|
116
121
|
* Envia o e-mail de aviso de segurança ao e-mail ATUAL quando uma troca de
|
|
117
122
|
* e-mail é solicitada ("Alguém pediu troca para X — se não foi você...").
|
|
@@ -149,6 +154,19 @@ export declare function sendSecurityNoticeEmail(ctx: HttpContext, cfg: ResolvedS
|
|
|
149
154
|
export declare function sendOtpUnlockEmail(ctx: HttpContext, data: {
|
|
150
155
|
email: string;
|
|
151
156
|
unlockUrl: string;
|
|
157
|
+
}, cfg?: ResolvedServerConfig): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Envia o aviso "sua conta será desativada por inatividade em N dias" pelo mailer
|
|
160
|
+
* default do host, com branding e i18n. Chamado pelo `authkit:accounts:expire-scan --warn`
|
|
161
|
+
* quando o host não define `mail.onAccountExpirationWarning` — fora de uma request, então
|
|
162
|
+
* aceita só um `{ logger }` (pino-like) no lugar do `HttpContext`, e o config resolvido
|
|
163
|
+
* vem do comando. Best-effort: sem `@adonisjs/mail` loga o aviso; nunca lança.
|
|
164
|
+
*/
|
|
165
|
+
export declare function sendAccountExpirationWarningEmail(ctx: HttpContext | {
|
|
166
|
+
logger: Pick<HttpContext['logger'], 'info' | 'error'>;
|
|
167
|
+
}, cfg: ResolvedServerConfig, data: {
|
|
168
|
+
email: string;
|
|
169
|
+
expiresInDays: number;
|
|
152
170
|
}): Promise<void>;
|
|
153
171
|
/**
|
|
154
172
|
* Sends the organization invitation email through the host default mailer.
|
|
@@ -170,7 +188,7 @@ export declare function sendOrgInvitationEmail(ctx: HttpContext, data: {
|
|
|
170
188
|
role: string;
|
|
171
189
|
acceptUrl: string;
|
|
172
190
|
token: string;
|
|
173
|
-
}): Promise<void>;
|
|
191
|
+
}, cfg?: ResolvedServerConfig): Promise<void>;
|
|
174
192
|
/**
|
|
175
193
|
* Envia o e-mail de verificação pelo mailer default do host.
|
|
176
194
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
@@ -178,5 +196,5 @@ export declare function sendOrgInvitationEmail(ctx: HttpContext, data: {
|
|
|
178
196
|
export declare function sendEmailVerificationEmail(ctx: HttpContext, data: {
|
|
179
197
|
email: string;
|
|
180
198
|
verifyUrl: string;
|
|
181
|
-
}): Promise<void>;
|
|
199
|
+
}, cfg?: ResolvedServerConfig): Promise<void>;
|
|
182
200
|
export {};
|
|
@@ -6,7 +6,9 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { getBootedApp } from '../../services/booted_app.js';
|
|
9
10
|
import { accountPath } from './account_paths.js';
|
|
11
|
+
import { resolveDefaultBrand } from './branding.js';
|
|
10
12
|
import { renderTransactionalEmail } from './email_templates.js';
|
|
11
13
|
import { resolveMessages, translate } from './i18n.js';
|
|
12
14
|
import { authkitOrigin } from './origin.js';
|
|
@@ -38,76 +40,115 @@ export function __setMailLoaderForTests(fn) {
|
|
|
38
40
|
mailServicePromise = undefined;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
43
|
+
let warnedUnresolved = false;
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
45
|
+
* Permite rearmar o warning "config não resolvido" (usado em testes).
|
|
46
|
+
* @internal
|
|
44
47
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
48
|
+
export function __resetMailConfigWarningForTests() {
|
|
49
|
+
warnedUnresolved = false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Encontra o config RESOLVIDO do authkit (`ResolvedServerConfig`).
|
|
53
|
+
*
|
|
54
|
+
* Por que não `ctx.containerResolver.app.config.get('authkit')`, como antes: o
|
|
55
|
+
* `ContainerResolver` do `@adonisjs/fold` não tem `.app` — então numa request real
|
|
56
|
+
* esse caminho dava `undefined` SEMPRE e todo e-mail default saía em `en` com a marca
|
|
57
|
+
* "AuthKit". E mesmo onde há um `app` (o ctx do SDK embedded), `config.get('authkit')`
|
|
58
|
+
* devolve o `ConfigProvider` preguiçoso do `defineConfig`, não o config resolvido —
|
|
59
|
+
* `.i18n`/`.branding`/`.mail` nunca existem nele.
|
|
60
|
+
*
|
|
61
|
+
* Ordem: o `cfg` que o chamador já tem → o binding `authkit.server` pelo container da
|
|
62
|
+
* request → pelo `app.container` do ctx → pelo app bootado capturado pelo provider.
|
|
63
|
+
*/
|
|
64
|
+
async function resolveServerConfig(ctx, cfg) {
|
|
65
|
+
if (cfg)
|
|
66
|
+
return cfg;
|
|
67
|
+
const resolver = ctx?.containerResolver;
|
|
68
|
+
const containers = [
|
|
69
|
+
() => resolver,
|
|
70
|
+
() => resolver?.app?.container,
|
|
71
|
+
() => getBootedApp().container,
|
|
72
|
+
];
|
|
73
|
+
for (const container of containers) {
|
|
74
|
+
try {
|
|
75
|
+
const maker = container();
|
|
76
|
+
if (typeof maker?.make !== 'function')
|
|
77
|
+
continue;
|
|
78
|
+
const service = (await maker.make('authkit.server'));
|
|
79
|
+
if (service?.config)
|
|
80
|
+
return service.config;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// tenta o próximo; a falha final é avisada em `resolveMailSettings`.
|
|
84
|
+
}
|
|
65
85
|
}
|
|
66
86
|
return undefined;
|
|
67
87
|
}
|
|
68
88
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
89
|
+
* O `from` global do `config/mail.ts` do host (um objeto simples, não um provider),
|
|
90
|
+
* lido do `app` do ctx (SDK embedded) ou do app bootado. Ausente → `undefined`, e o
|
|
91
|
+
* `@adonisjs/mail` decide.
|
|
72
92
|
*/
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
function hostMailFrom(ctx) {
|
|
94
|
+
const apps = [
|
|
95
|
+
() => ctx?.containerResolver?.app,
|
|
96
|
+
() => getBootedApp(),
|
|
97
|
+
];
|
|
98
|
+
for (const app of apps) {
|
|
99
|
+
try {
|
|
100
|
+
const from = app()?.config?.get?.('mail')?.from;
|
|
101
|
+
if (from)
|
|
102
|
+
return from;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// sem app/config de mail — tenta o próximo.
|
|
83
106
|
}
|
|
84
107
|
}
|
|
85
|
-
|
|
86
|
-
// sem config authkit resolvível — usa default neutro.
|
|
87
|
-
}
|
|
88
|
-
return { appName: 'AuthKit' };
|
|
108
|
+
return undefined;
|
|
89
109
|
}
|
|
90
110
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
111
|
+
* Marca, i18n e `from` dos e-mails default, a partir do config resolvido.
|
|
112
|
+
*
|
|
113
|
+
* Sem config resolvível cai num default neutro (`en`, "AuthKit") — mas NUNCA em
|
|
114
|
+
* silêncio: avisa uma vez por processo, porque um e-mail no idioma/marca errados é
|
|
115
|
+
* exatamente o tipo de bug que só aparece na caixa de entrada de um usuário.
|
|
94
116
|
*/
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
async function resolveMailSettings(ctx, cfg) {
|
|
118
|
+
const resolved = await resolveServerConfig(ctx, cfg);
|
|
119
|
+
if (!resolved) {
|
|
120
|
+
if (!warnedUnresolved) {
|
|
121
|
+
warnedUnresolved = true;
|
|
122
|
+
const message = 'authkit: config do authkit não resolvido para os e-mails default — usando locale `en` e a marca "AuthKit". Registre o AuthkitServerProvider ou passe o config resolvido.';
|
|
123
|
+
if (typeof ctx?.logger?.warn === 'function')
|
|
124
|
+
ctx.logger.warn(message);
|
|
125
|
+
else
|
|
126
|
+
console.warn(message);
|
|
127
|
+
}
|
|
128
|
+
// `en` de propósito: aqui NÃO há config nenhum (nem o default `pt-BR` do `defineConfig`,
|
|
129
|
+
// que um host sem `i18n` recebe pelo `cfg.locale` acima). Sem saber de quem é o app,
|
|
130
|
+
// o catálogo base — o único com todas as chaves — é o fallback honesto.
|
|
131
|
+
return { brand: { appName: 'AuthKit' }, messages: resolveMessages(), locale: 'en' };
|
|
132
|
+
}
|
|
133
|
+
const branding = resolved.branding;
|
|
134
|
+
const defaultBrand = resolveDefaultBrand(branding);
|
|
135
|
+
return {
|
|
136
|
+
brand: {
|
|
137
|
+
appName: defaultBrand.appName,
|
|
138
|
+
accent: defaultBrand.accent,
|
|
139
|
+
company: branding?.company,
|
|
140
|
+
},
|
|
141
|
+
messages: resolved.messages ?? resolveMessages(),
|
|
142
|
+
locale: resolved.locale ?? 'en',
|
|
143
|
+
// `from` próprio do authkit tem prioridade; senão o `from` global do `config/mail.ts`.
|
|
144
|
+
from: resolved.mail?.from ?? hostMailFrom(ctx),
|
|
145
|
+
};
|
|
105
146
|
}
|
|
106
|
-
async function sendEmail(
|
|
147
|
+
async function sendEmail(settings, to, content) {
|
|
107
148
|
const mail = await loadMail();
|
|
108
149
|
if (!mail)
|
|
109
150
|
return false;
|
|
110
|
-
const from =
|
|
151
|
+
const { from } = settings;
|
|
111
152
|
await mail.send((message) => {
|
|
112
153
|
if (from)
|
|
113
154
|
message.from(from);
|
|
@@ -119,10 +160,10 @@ async function sendEmail(ctx, to, content) {
|
|
|
119
160
|
* Envia o e-mail de redefinição de senha pelo mailer default do host.
|
|
120
161
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
121
162
|
*/
|
|
122
|
-
export async function sendPasswordResetEmail(ctx, data) {
|
|
163
|
+
export async function sendPasswordResetEmail(ctx, data, cfg) {
|
|
123
164
|
try {
|
|
124
|
-
const
|
|
125
|
-
const { messages: t, locale } =
|
|
165
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
166
|
+
const { brand, messages: t, locale } = settings;
|
|
126
167
|
const content = renderTransactionalEmail({
|
|
127
168
|
brand,
|
|
128
169
|
locale,
|
|
@@ -134,7 +175,7 @@ export async function sendPasswordResetEmail(ctx, data) {
|
|
|
134
175
|
ctaUrl: data.resetUrl,
|
|
135
176
|
footnote: translate(t, 'mail.reset.fallback'),
|
|
136
177
|
});
|
|
137
|
-
const sent = await sendEmail(
|
|
178
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
138
179
|
if (!sent) {
|
|
139
180
|
ctx.logger.info({ resetUrl: data.resetUrl, email: data.email }, 'authkit: link de redefinição de senha (dev — @adonisjs/mail ausente)');
|
|
140
181
|
}
|
|
@@ -147,10 +188,10 @@ export async function sendPasswordResetEmail(ctx, data) {
|
|
|
147
188
|
* Envia o e-mail de confirmação de TROCA de e-mail para o NOVO endereço.
|
|
148
189
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
149
190
|
*/
|
|
150
|
-
export async function sendEmailChangeConfirmationEmail(ctx, data) {
|
|
191
|
+
export async function sendEmailChangeConfirmationEmail(ctx, data, cfg) {
|
|
151
192
|
try {
|
|
152
|
-
const
|
|
153
|
-
const { messages: t, locale } =
|
|
193
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
194
|
+
const { brand, messages: t, locale } = settings;
|
|
154
195
|
const content = renderTransactionalEmail({
|
|
155
196
|
brand,
|
|
156
197
|
locale,
|
|
@@ -162,7 +203,7 @@ export async function sendEmailChangeConfirmationEmail(ctx, data) {
|
|
|
162
203
|
ctaUrl: data.confirmUrl,
|
|
163
204
|
footnote: translate(t, 'mail.email_change.fallback'),
|
|
164
205
|
});
|
|
165
|
-
const sent = await sendEmail(
|
|
206
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
166
207
|
if (!sent) {
|
|
167
208
|
ctx.logger.info({ confirmUrl: data.confirmUrl, email: data.email }, 'authkit: link de confirmação de troca de e-mail (dev — @adonisjs/mail ausente)');
|
|
168
209
|
}
|
|
@@ -177,8 +218,8 @@ export async function sendEmailChangeConfirmationEmail(ctx, data) {
|
|
|
177
218
|
*/
|
|
178
219
|
export async function sendNewLoginEmail(ctx, cfg, data) {
|
|
179
220
|
try {
|
|
180
|
-
const
|
|
181
|
-
const { messages: t, locale } =
|
|
221
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
222
|
+
const { brand, messages: t, locale } = settings;
|
|
182
223
|
const origin = authkitOrigin(cfg);
|
|
183
224
|
const intro = [
|
|
184
225
|
translate(t, 'mail.new_login.intro'),
|
|
@@ -196,7 +237,7 @@ export async function sendNewLoginEmail(ctx, cfg, data) {
|
|
|
196
237
|
ctaUrl: `${origin}${accountPath('security')}`,
|
|
197
238
|
footnote: translate(t, 'mail.new_login.fallback'),
|
|
198
239
|
});
|
|
199
|
-
const sent = await sendEmail(
|
|
240
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
200
241
|
if (!sent) {
|
|
201
242
|
ctx.logger.info({ ip: data.ip, email: data.email }, 'authkit: alerta de novo acesso (dev — @adonisjs/mail ausente)');
|
|
202
243
|
}
|
|
@@ -211,8 +252,8 @@ export async function sendNewLoginEmail(ctx, cfg, data) {
|
|
|
211
252
|
*/
|
|
212
253
|
export async function sendNewDeviceLoginEmail(ctx, cfg, data) {
|
|
213
254
|
try {
|
|
214
|
-
const
|
|
215
|
-
const { messages: t, locale } =
|
|
255
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
256
|
+
const { brand, messages: t, locale } = settings;
|
|
216
257
|
const origin = authkitOrigin(cfg);
|
|
217
258
|
const lines = [
|
|
218
259
|
translate(t, 'mail.new_login.intro'),
|
|
@@ -233,7 +274,7 @@ export async function sendNewDeviceLoginEmail(ctx, cfg, data) {
|
|
|
233
274
|
ctaUrl: `${origin}${accountPath('security')}`,
|
|
234
275
|
footnote: translate(t, 'mail.new_login.fallback'),
|
|
235
276
|
});
|
|
236
|
-
const sent = await sendEmail(
|
|
277
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
237
278
|
if (!sent) {
|
|
238
279
|
ctx.logger.info({ ip: data.ip, email: data.email, userAgent: data.userAgent }, 'authkit: alerta de novo dispositivo (dev — @adonisjs/mail ausente)');
|
|
239
280
|
}
|
|
@@ -246,10 +287,10 @@ export async function sendNewDeviceLoginEmail(ctx, cfg, data) {
|
|
|
246
287
|
* Envia o e-mail de magic link (login passwordless) pelo mailer default do host.
|
|
247
288
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
248
289
|
*/
|
|
249
|
-
export async function sendMagicLinkEmail(ctx, data) {
|
|
290
|
+
export async function sendMagicLinkEmail(ctx, data, cfg) {
|
|
250
291
|
try {
|
|
251
|
-
const
|
|
252
|
-
const { messages: t, locale } =
|
|
292
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
293
|
+
const { brand, messages: t, locale } = settings;
|
|
253
294
|
// Login choose-first: o `channel` decide o que o e-mail SURFA (os dois tokens
|
|
254
295
|
// continuam emitidos co-locados a montante — isto é só renderização).
|
|
255
296
|
// - 'code' → e-mail SÓ com o código (sem botão/link), quando há código;
|
|
@@ -285,7 +326,7 @@ export async function sendMagicLinkEmail(ctx, data) {
|
|
|
285
326
|
code: linkOnly ? undefined : data.code,
|
|
286
327
|
codeLabel: translate(t, 'mail.magic_link.code_label'),
|
|
287
328
|
});
|
|
288
|
-
const sent = await sendEmail(
|
|
329
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
289
330
|
if (!sent) {
|
|
290
331
|
ctx.logger.info({ magicUrl: data.magicUrl, email: data.email }, 'authkit: magic link de login (dev — @adonisjs/mail ausente)');
|
|
291
332
|
}
|
|
@@ -303,10 +344,10 @@ export async function sendMagicLinkEmail(ctx, data) {
|
|
|
303
344
|
*
|
|
304
345
|
* Estável no 1.x (é o mailer das `personalAgents.extensions`).
|
|
305
346
|
*/
|
|
306
|
-
export async function sendAuthkitCodeEmail(ctx, data) {
|
|
347
|
+
export async function sendAuthkitCodeEmail(ctx, data, cfg) {
|
|
307
348
|
try {
|
|
308
|
-
const
|
|
309
|
-
const { messages: t, locale } =
|
|
349
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
350
|
+
const { brand, messages: t, locale } = settings;
|
|
310
351
|
const key = (name) => `${data.messages}.${name}`;
|
|
311
352
|
const content = renderTransactionalEmail({
|
|
312
353
|
brand,
|
|
@@ -318,7 +359,7 @@ export async function sendAuthkitCodeEmail(ctx, data) {
|
|
|
318
359
|
code: data.code,
|
|
319
360
|
codeLabel: translate(t, key('code_label'), data.params),
|
|
320
361
|
});
|
|
321
|
-
const sent = await sendEmail(
|
|
362
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
322
363
|
if (!sent) {
|
|
323
364
|
ctx.logger.info({ email: data.email, code: data.code, messages: data.messages, ...(data.params ?? {}) }, 'authkit: código por e-mail (dev — @adonisjs/mail ausente)');
|
|
324
365
|
}
|
|
@@ -353,10 +394,10 @@ export async function sendAuthkitCodeEmail(ctx, data) {
|
|
|
353
394
|
* apagar o pendente da sessão e recusar — flashar "link enviado" deixaria o
|
|
354
395
|
* usuário esperando um e-mail que nunca vem, na tela que já é o gargalo dele.
|
|
355
396
|
*/
|
|
356
|
-
export async function sendSudoLinkEmail(ctx, data) {
|
|
397
|
+
export async function sendSudoLinkEmail(ctx, data, cfg) {
|
|
357
398
|
try {
|
|
358
|
-
const
|
|
359
|
-
const { messages: t, locale } =
|
|
399
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
400
|
+
const { brand, messages: t, locale } = settings;
|
|
360
401
|
const content = renderTransactionalEmail({
|
|
361
402
|
brand,
|
|
362
403
|
locale,
|
|
@@ -368,7 +409,7 @@ export async function sendSudoLinkEmail(ctx, data) {
|
|
|
368
409
|
ctaUrl: data.sudoUrl,
|
|
369
410
|
footnote: translate(t, 'mail.sudo_link.fallback'),
|
|
370
411
|
});
|
|
371
|
-
const sent = await sendEmail(
|
|
412
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
372
413
|
if (!sent) {
|
|
373
414
|
ctx.logger?.info({ sudoUrl: data.sudoUrl, email: data.email }, 'authkit: link de confirmação de identidade (dev — @adonisjs/mail ausente)');
|
|
374
415
|
}
|
|
@@ -386,8 +427,8 @@ export async function sendSudoLinkEmail(ctx, data) {
|
|
|
386
427
|
*/
|
|
387
428
|
export async function sendEmailChangeNoticeEmail(ctx, cfg, data) {
|
|
388
429
|
try {
|
|
389
|
-
const
|
|
390
|
-
const { messages: t, locale } =
|
|
430
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
431
|
+
const { brand, messages: t, locale } = settings;
|
|
391
432
|
const origin = authkitOrigin(cfg);
|
|
392
433
|
const content = renderTransactionalEmail({
|
|
393
434
|
brand,
|
|
@@ -400,7 +441,7 @@ export async function sendEmailChangeNoticeEmail(ctx, cfg, data) {
|
|
|
400
441
|
ctaUrl: `${origin}${accountPath('security')}`,
|
|
401
442
|
footnote: translate(t, 'mail.email_change_notice.fallback'),
|
|
402
443
|
});
|
|
403
|
-
const sent = await sendEmail(
|
|
444
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
404
445
|
if (!sent) {
|
|
405
446
|
ctx.logger.info({ newEmail: data.newEmail, email: data.email }, 'authkit: aviso de troca de e-mail (dev — @adonisjs/mail ausente)');
|
|
406
447
|
}
|
|
@@ -416,8 +457,8 @@ export async function sendEmailChangeNoticeEmail(ctx, cfg, data) {
|
|
|
416
457
|
*/
|
|
417
458
|
export async function sendEmailChangedCompletedEmail(ctx, cfg, data) {
|
|
418
459
|
try {
|
|
419
|
-
const
|
|
420
|
-
const { messages: t, locale } =
|
|
460
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
461
|
+
const { brand, messages: t, locale } = settings;
|
|
421
462
|
const origin = authkitOrigin(cfg);
|
|
422
463
|
const content = renderTransactionalEmail({
|
|
423
464
|
brand,
|
|
@@ -433,7 +474,7 @@ export async function sendEmailChangedCompletedEmail(ctx, cfg, data) {
|
|
|
433
474
|
ctaUrl: `${origin}${accountPath('security')}`,
|
|
434
475
|
footnote: translate(t, 'mail.email_changed_completed.fallback'),
|
|
435
476
|
});
|
|
436
|
-
const sent = await sendEmail(
|
|
477
|
+
const sent = await sendEmail(settings, data.oldEmail, content);
|
|
437
478
|
if (!sent) {
|
|
438
479
|
ctx.logger.info({ oldEmail: data.oldEmail, newEmail: data.newEmail }, 'authkit: confirmação de troca de e-mail concluída (dev — @adonisjs/mail ausente)');
|
|
439
480
|
}
|
|
@@ -449,8 +490,8 @@ export async function sendEmailChangedCompletedEmail(ctx, cfg, data) {
|
|
|
449
490
|
*/
|
|
450
491
|
export async function sendSecurityNoticeEmail(ctx, cfg, data) {
|
|
451
492
|
try {
|
|
452
|
-
const
|
|
453
|
-
const { messages: t, locale } =
|
|
493
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
494
|
+
const { brand, messages: t, locale } = settings;
|
|
454
495
|
const origin = authkitOrigin(cfg);
|
|
455
496
|
const kindKey = `mail.security_notice.kind_${data.kind}`;
|
|
456
497
|
const kindLabel = translate(t, kindKey);
|
|
@@ -471,7 +512,7 @@ export async function sendSecurityNoticeEmail(ctx, cfg, data) {
|
|
|
471
512
|
ctaUrl: `${origin}${accountPath('security')}`,
|
|
472
513
|
footnote: translate(t, 'mail.security_notice.fallback'),
|
|
473
514
|
});
|
|
474
|
-
const sent = await sendEmail(
|
|
515
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
475
516
|
if (!sent) {
|
|
476
517
|
ctx.logger.info({ kind: data.kind, email: data.email }, 'authkit: notificação de segurança (dev — @adonisjs/mail ausente)');
|
|
477
518
|
}
|
|
@@ -484,10 +525,10 @@ export async function sendSecurityNoticeEmail(ctx, cfg, data) {
|
|
|
484
525
|
* Envia o e-mail de desbloqueio do fator OTP (quando TOTP/recovery excede maxAttempts).
|
|
485
526
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
486
527
|
*/
|
|
487
|
-
export async function sendOtpUnlockEmail(ctx, data) {
|
|
528
|
+
export async function sendOtpUnlockEmail(ctx, data, cfg) {
|
|
488
529
|
try {
|
|
489
|
-
const
|
|
490
|
-
const { messages: t, locale } =
|
|
530
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
531
|
+
const { brand, messages: t, locale } = settings;
|
|
491
532
|
const content = renderTransactionalEmail({
|
|
492
533
|
brand,
|
|
493
534
|
locale,
|
|
@@ -499,7 +540,7 @@ export async function sendOtpUnlockEmail(ctx, data) {
|
|
|
499
540
|
ctaUrl: data.unlockUrl,
|
|
500
541
|
footnote: translate(t, 'mail.otp_unlock.fallback'),
|
|
501
542
|
});
|
|
502
|
-
const sent = await sendEmail(
|
|
543
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
503
544
|
if (!sent) {
|
|
504
545
|
ctx.logger.info({ unlockUrl: data.unlockUrl, email: data.email }, 'authkit: link de desbloqueio OTP (dev — @adonisjs/mail ausente)');
|
|
505
546
|
}
|
|
@@ -508,6 +549,39 @@ export async function sendOtpUnlockEmail(ctx, data) {
|
|
|
508
549
|
ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar e-mail de desbloqueio OTP');
|
|
509
550
|
}
|
|
510
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* Envia o aviso "sua conta será desativada por inatividade em N dias" pelo mailer
|
|
554
|
+
* default do host, com branding e i18n. Chamado pelo `authkit:accounts:expire-scan --warn`
|
|
555
|
+
* quando o host não define `mail.onAccountExpirationWarning` — fora de uma request, então
|
|
556
|
+
* aceita só um `{ logger }` (pino-like) no lugar do `HttpContext`, e o config resolvido
|
|
557
|
+
* vem do comando. Best-effort: sem `@adonisjs/mail` loga o aviso; nunca lança.
|
|
558
|
+
*/
|
|
559
|
+
export async function sendAccountExpirationWarningEmail(ctx, cfg, data) {
|
|
560
|
+
const httpCtx = ctx;
|
|
561
|
+
try {
|
|
562
|
+
const settings = await resolveMailSettings(httpCtx, cfg);
|
|
563
|
+
const { brand, messages: t, locale } = settings;
|
|
564
|
+
const origin = authkitOrigin(cfg);
|
|
565
|
+
const content = renderTransactionalEmail({
|
|
566
|
+
brand,
|
|
567
|
+
locale,
|
|
568
|
+
linkFallback: translate(t, 'mail.common.link_fallback'),
|
|
569
|
+
subject: translate(t, 'mail.account_expiration.subject', { days: data.expiresInDays }),
|
|
570
|
+
heading: translate(t, 'mail.account_expiration.heading', { days: data.expiresInDays }),
|
|
571
|
+
intro: translate(t, 'mail.account_expiration.intro', { days: data.expiresInDays }),
|
|
572
|
+
ctaLabel: translate(t, 'mail.account_expiration.cta'),
|
|
573
|
+
ctaUrl: `${origin}${accountPath('login')}`,
|
|
574
|
+
footnote: translate(t, 'mail.account_expiration.fallback'),
|
|
575
|
+
});
|
|
576
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
577
|
+
if (!sent) {
|
|
578
|
+
httpCtx.logger?.info({ email: data.email, expiresInDays: data.expiresInDays }, 'authkit: aviso de expiração por inatividade (dev — @adonisjs/mail ausente)');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
httpCtx.logger?.error({ err: error, email: data.email }, 'authkit: falha ao enviar aviso de expiração por inatividade');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
511
585
|
/**
|
|
512
586
|
* Sends the organization invitation email through the host default mailer.
|
|
513
587
|
*
|
|
@@ -520,10 +594,10 @@ export async function sendOtpUnlockEmail(ctx, data) {
|
|
|
520
594
|
*
|
|
521
595
|
* Best-effort: never throws — invitation creation must not depend on delivery.
|
|
522
596
|
*/
|
|
523
|
-
export async function sendOrgInvitationEmail(ctx, data) {
|
|
597
|
+
export async function sendOrgInvitationEmail(ctx, data, cfg) {
|
|
524
598
|
try {
|
|
525
|
-
const
|
|
526
|
-
const { messages: t, locale } =
|
|
599
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
600
|
+
const { brand, messages: t, locale } = settings;
|
|
527
601
|
const content = renderTransactionalEmail({
|
|
528
602
|
brand,
|
|
529
603
|
locale,
|
|
@@ -535,7 +609,7 @@ export async function sendOrgInvitationEmail(ctx, data) {
|
|
|
535
609
|
ctaUrl: data.acceptUrl,
|
|
536
610
|
footnote: translate(t, 'mail.org_invitation.fallback'),
|
|
537
611
|
});
|
|
538
|
-
const sent = await sendEmail(
|
|
612
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
539
613
|
if (!sent) {
|
|
540
614
|
ctx.logger?.info({
|
|
541
615
|
acceptUrl: data.acceptUrl,
|
|
@@ -553,10 +627,10 @@ export async function sendOrgInvitationEmail(ctx, data) {
|
|
|
553
627
|
* Envia o e-mail de verificação pelo mailer default do host.
|
|
554
628
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
555
629
|
*/
|
|
556
|
-
export async function sendEmailVerificationEmail(ctx, data) {
|
|
630
|
+
export async function sendEmailVerificationEmail(ctx, data, cfg) {
|
|
557
631
|
try {
|
|
558
|
-
const
|
|
559
|
-
const { messages: t, locale } =
|
|
632
|
+
const settings = await resolveMailSettings(ctx, cfg);
|
|
633
|
+
const { brand, messages: t, locale } = settings;
|
|
560
634
|
const content = renderTransactionalEmail({
|
|
561
635
|
brand,
|
|
562
636
|
locale,
|
|
@@ -567,7 +641,7 @@ export async function sendEmailVerificationEmail(ctx, data) {
|
|
|
567
641
|
ctaLabel: translate(t, 'mail.verify.cta'),
|
|
568
642
|
ctaUrl: data.verifyUrl,
|
|
569
643
|
});
|
|
570
|
-
const sent = await sendEmail(
|
|
644
|
+
const sent = await sendEmail(settings, data.email, content);
|
|
571
645
|
if (!sent) {
|
|
572
646
|
ctx.logger.info({ verifyUrl: data.verifyUrl, email: data.email }, 'authkit: link de verificação de e-mail (dev — @adonisjs/mail ausente)');
|
|
573
647
|
}
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -629,6 +629,11 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
629
629
|
'mail.otp_unlock.intro': string;
|
|
630
630
|
'mail.otp_unlock.cta': string;
|
|
631
631
|
'mail.otp_unlock.fallback': string;
|
|
632
|
+
'mail.account_expiration.subject': string;
|
|
633
|
+
'mail.account_expiration.heading': string;
|
|
634
|
+
'mail.account_expiration.intro': string;
|
|
635
|
+
'mail.account_expiration.cta': string;
|
|
636
|
+
'mail.account_expiration.fallback': string;
|
|
632
637
|
'account.confirm.page_title': string;
|
|
633
638
|
'account.confirm.title': string;
|
|
634
639
|
'account.confirm.intro': string;
|
|
@@ -1422,6 +1427,11 @@ export declare const PT_BR_MESSAGES: {
|
|
|
1422
1427
|
'mail.otp_unlock.intro': string;
|
|
1423
1428
|
'mail.otp_unlock.cta': string;
|
|
1424
1429
|
'mail.otp_unlock.fallback': string;
|
|
1430
|
+
'mail.account_expiration.subject': string;
|
|
1431
|
+
'mail.account_expiration.heading': string;
|
|
1432
|
+
'mail.account_expiration.intro': string;
|
|
1433
|
+
'mail.account_expiration.cta': string;
|
|
1434
|
+
'mail.account_expiration.fallback': string;
|
|
1425
1435
|
'otp_unlock.page_title': string;
|
|
1426
1436
|
'otp_unlock.ok_title': string;
|
|
1427
1437
|
'otp_unlock.ok_body': string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -684,6 +684,12 @@ export const DEFAULT_MESSAGES = {
|
|
|
684
684
|
'mail.otp_unlock.intro': 'Your two-factor authentication factor has been locked due to too many failed attempts. Click the button below to unlock it.',
|
|
685
685
|
'mail.otp_unlock.cta': 'Unlock two-factor',
|
|
686
686
|
'mail.otp_unlock.fallback': 'If you did not attempt to sign in, your account may be at risk — change your password immediately.',
|
|
687
|
+
// E-mail de aviso de expiração por inatividade (`authkit:accounts:expire-scan --warn`).
|
|
688
|
+
'mail.account_expiration.subject': 'Your account will be deactivated due to inactivity',
|
|
689
|
+
'mail.account_expiration.heading': 'Your account will be deactivated soon',
|
|
690
|
+
'mail.account_expiration.intro': 'Your account will be deactivated in {days} day(s) due to inactivity. Sign in to keep your account active.',
|
|
691
|
+
'mail.account_expiration.cta': 'Sign in',
|
|
692
|
+
'mail.account_expiration.fallback': 'If you no longer use this account, you can ignore this email and it will be deactivated.',
|
|
687
693
|
// Tela de desbloqueio OTP (/auth/otp-unlock/:token).
|
|
688
694
|
// Sudo mode (confirm identity — /account/confirm).
|
|
689
695
|
'account.confirm.page_title': 'Confirm your identity',
|
|
@@ -1561,6 +1567,12 @@ export const PT_BR_MESSAGES = {
|
|
|
1561
1567
|
'mail.otp_unlock.intro': 'Sua verificação em duas etapas foi bloqueada após muitas tentativas incorretas. Clique no botão abaixo para desbloquear.',
|
|
1562
1568
|
'mail.otp_unlock.cta': 'Desbloquear verificação',
|
|
1563
1569
|
'mail.otp_unlock.fallback': 'Se você não tentou fazer login, sua conta pode estar em risco — altere sua senha imediatamente.',
|
|
1570
|
+
// E-mail de aviso de expiração por inatividade (pt-BR).
|
|
1571
|
+
'mail.account_expiration.subject': 'Sua conta será desativada por inatividade',
|
|
1572
|
+
'mail.account_expiration.heading': 'Sua conta será desativada em breve',
|
|
1573
|
+
'mail.account_expiration.intro': 'Sua conta será desativada em {days} dia(s) por inatividade. Entre na sua conta para mantê-la ativa.',
|
|
1574
|
+
'mail.account_expiration.cta': 'Entrar',
|
|
1575
|
+
'mail.account_expiration.fallback': 'Se você não usa mais esta conta, pode ignorar este e-mail e ela será desativada.',
|
|
1564
1576
|
// Tela de desbloqueio OTP (pt-BR).
|
|
1565
1577
|
'otp_unlock.page_title': 'Desbloqueio da verificação em duas etapas',
|
|
1566
1578
|
'otp_unlock.ok_title': 'Verificação em duas etapas desbloqueada',
|
|
@@ -154,7 +154,7 @@ export function magicLink() {
|
|
|
154
154
|
delivered = true;
|
|
155
155
|
}
|
|
156
156
|
else {
|
|
157
|
-
delivered = await sendSudoLinkEmail(ctx, { email: c.account.email, sudoUrl });
|
|
157
|
+
delivered = await sendSudoLinkEmail(ctx, { email: c.account.email, sudoUrl }, c.cfg);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.15",
|
|
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",
|