@dudousxd/adonis-authkit-server 0.1.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.
Files changed (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/build/assets/grafana/authkit-dashboard.json +118 -0
  4. package/build/commands/commands.json +30 -0
  5. package/build/commands/configure.d.ts +2 -0
  6. package/build/commands/configure.js +42 -0
  7. package/build/commands/eject.d.ts +11 -0
  8. package/build/commands/eject.js +96 -0
  9. package/build/commands/main.d.ts +12 -0
  10. package/build/commands/main.js +38 -0
  11. package/build/commands/ui_preset.d.ts +4 -0
  12. package/build/commands/ui_preset.js +32 -0
  13. package/build/database/migrations/make_authkit_oidc_table.d.ts +6 -0
  14. package/build/database/migrations/make_authkit_oidc_table.js +19 -0
  15. package/build/host/views/account/login.edge +29 -0
  16. package/build/host/views/account/mfa.edge +151 -0
  17. package/build/host/views/account/tokens.edge +70 -0
  18. package/build/host/views/admin/audit.edge +72 -0
  19. package/build/host/views/admin/clients.edge +51 -0
  20. package/build/host/views/admin/dashboard.edge +58 -0
  21. package/build/host/views/admin/users.edge +76 -0
  22. package/build/host/views/consent.edge +19 -0
  23. package/build/host/views/forgot.edge +30 -0
  24. package/build/host/views/login.edge +91 -0
  25. package/build/host/views/mfa-challenge.edge +88 -0
  26. package/build/host/views/reset.edge +29 -0
  27. package/build/host/views/signup.edge +44 -0
  28. package/build/host/views/verify-email.edge +16 -0
  29. package/build/index.d.ts +42 -0
  30. package/build/index.js +28 -0
  31. package/build/providers/authkit_server_provider.d.ts +19 -0
  32. package/build/providers/authkit_server_provider.js +81 -0
  33. package/build/src/accounts/account_store.d.ts +136 -0
  34. package/build/src/accounts/account_store.js +1 -0
  35. package/build/src/accounts/lucid_account_store.d.ts +75 -0
  36. package/build/src/accounts/lucid_account_store.js +396 -0
  37. package/build/src/adapters/adapter_contract.d.ts +18 -0
  38. package/build/src/adapters/adapter_contract.js +1 -0
  39. package/build/src/adapters/database_adapter.d.ts +15 -0
  40. package/build/src/adapters/database_adapter.js +63 -0
  41. package/build/src/adapters/factory.d.ts +30 -0
  42. package/build/src/adapters/factory.js +43 -0
  43. package/build/src/adapters/redis_adapter.d.ts +16 -0
  44. package/build/src/adapters/redis_adapter.js +95 -0
  45. package/build/src/audit/audit_sink.d.ts +54 -0
  46. package/build/src/audit/audit_sink.js +1 -0
  47. package/build/src/audit/lucid_audit_sink.d.ts +10 -0
  48. package/build/src/audit/lucid_audit_sink.js +60 -0
  49. package/build/src/controllers/oidc_callback_controller.d.ts +22 -0
  50. package/build/src/controllers/oidc_callback_controller.js +33 -0
  51. package/build/src/define_config.d.ts +261 -0
  52. package/build/src/define_config.js +115 -0
  53. package/build/src/host/account_lockout.d.ts +86 -0
  54. package/build/src/host/account_lockout.js +185 -0
  55. package/build/src/host/augmentations.d.ts +1 -0
  56. package/build/src/host/augmentations.js +1 -0
  57. package/build/src/host/branding.d.ts +17 -0
  58. package/build/src/host/branding.js +8 -0
  59. package/build/src/host/controllers/account_mfa_controller.d.ts +30 -0
  60. package/build/src/host/controllers/account_mfa_controller.js +157 -0
  61. package/build/src/host/controllers/account_session_controller.d.ts +7 -0
  62. package/build/src/host/controllers/account_session_controller.js +50 -0
  63. package/build/src/host/controllers/account_tokens_controller.d.ts +7 -0
  64. package/build/src/host/controllers/account_tokens_controller.js +55 -0
  65. package/build/src/host/controllers/admin/admin_audit_controller.d.ts +10 -0
  66. package/build/src/host/controllers/admin/admin_audit_controller.js +56 -0
  67. package/build/src/host/controllers/admin/admin_clients_controller.d.ts +10 -0
  68. package/build/src/host/controllers/admin/admin_clients_controller.js +24 -0
  69. package/build/src/host/controllers/admin/admin_dashboard_controller.d.ts +10 -0
  70. package/build/src/host/controllers/admin/admin_dashboard_controller.js +27 -0
  71. package/build/src/host/controllers/admin/admin_users_controller.d.ts +11 -0
  72. package/build/src/host/controllers/admin/admin_users_controller.js +53 -0
  73. package/build/src/host/controllers/interaction_controller.d.ts +44 -0
  74. package/build/src/host/controllers/interaction_controller.js +304 -0
  75. package/build/src/host/controllers/pat_introspection_controller.d.ts +22 -0
  76. package/build/src/host/controllers/pat_introspection_controller.js +46 -0
  77. package/build/src/host/controllers/registration_controller.d.ts +18 -0
  78. package/build/src/host/controllers/registration_controller.js +169 -0
  79. package/build/src/host/controllers/social_controller.d.ts +8 -0
  80. package/build/src/host/controllers/social_controller.js +82 -0
  81. package/build/src/host/default_mailer.d.ts +39 -0
  82. package/build/src/host/default_mailer.js +141 -0
  83. package/build/src/host/email_templates.d.ts +35 -0
  84. package/build/src/host/email_templates.js +66 -0
  85. package/build/src/host/i18n.d.ts +178 -0
  86. package/build/src/host/i18n.js +208 -0
  87. package/build/src/host/middleware/account_auth.d.ts +7 -0
  88. package/build/src/host/middleware/account_auth.js +11 -0
  89. package/build/src/host/rate_limit.d.ts +32 -0
  90. package/build/src/host/rate_limit.js +87 -0
  91. package/build/src/host/register_auth_host.d.ts +41 -0
  92. package/build/src/host/register_auth_host.js +133 -0
  93. package/build/src/host/renderers/edge_renderer.d.ts +3 -0
  94. package/build/src/host/renderers/edge_renderer.js +29 -0
  95. package/build/src/host/renderers/inertia_renderer.d.ts +5 -0
  96. package/build/src/host/renderers/inertia_renderer.js +26 -0
  97. package/build/src/host/validators.d.ts +39 -0
  98. package/build/src/host/validators.js +13 -0
  99. package/build/src/keys/jwks_manager.d.ts +6 -0
  100. package/build/src/keys/jwks_manager.js +11 -0
  101. package/build/src/mixins/with_audit_log.d.ts +19 -0
  102. package/build/src/mixins/with_audit_log.js +41 -0
  103. package/build/src/mixins/with_auth_user.d.ts +18 -0
  104. package/build/src/mixins/with_auth_user.js +39 -0
  105. package/build/src/mixins/with_credentials.d.ts +20 -0
  106. package/build/src/mixins/with_credentials.js +29 -0
  107. package/build/src/mixins/with_mfa.d.ts +31 -0
  108. package/build/src/mixins/with_mfa.js +39 -0
  109. package/build/src/mixins/with_personal_access_token.d.ts +19 -0
  110. package/build/src/mixins/with_personal_access_token.js +44 -0
  111. package/build/src/mixins/with_provider_identity.d.ts +20 -0
  112. package/build/src/mixins/with_provider_identity.js +32 -0
  113. package/build/src/mixins/with_webauthn_credential.d.ts +37 -0
  114. package/build/src/mixins/with_webauthn_credential.js +49 -0
  115. package/build/src/observability/metrics_controller.d.ts +5 -0
  116. package/build/src/observability/metrics_controller.js +24 -0
  117. package/build/src/observability/metrics_service.d.ts +2 -0
  118. package/build/src/observability/metrics_service.js +7 -0
  119. package/build/src/observability/otel_recorder.d.ts +10 -0
  120. package/build/src/observability/otel_recorder.js +59 -0
  121. package/build/src/observability/wire_provider_events.d.ts +12 -0
  122. package/build/src/observability/wire_provider_events.js +19 -0
  123. package/build/src/pat/lucid_pat_store.d.ts +6 -0
  124. package/build/src/pat/lucid_pat_store.js +62 -0
  125. package/build/src/pat/pat_store.d.ts +31 -0
  126. package/build/src/pat/pat_store.js +1 -0
  127. package/build/src/pat/pat_tokens.d.ts +4 -0
  128. package/build/src/pat/pat_tokens.js +9 -0
  129. package/build/src/provider/build_provider.d.ts +8 -0
  130. package/build/src/provider/build_provider.js +101 -0
  131. package/build/src/provider/interaction_actions.d.ts +21 -0
  132. package/build/src/provider/interaction_actions.js +32 -0
  133. package/build/src/provider/oidc_service.d.ts +17 -0
  134. package/build/src/provider/oidc_service.js +84 -0
  135. package/build/src/provider/token_exchange.d.ts +15 -0
  136. package/build/src/provider/token_exchange.js +72 -0
  137. package/build/src/register_routes.d.ts +16 -0
  138. package/build/src/register_routes.js +21 -0
  139. package/build/stubs/config/authkit.stub +29 -0
  140. package/build/stubs/main.d.ts +1 -0
  141. package/build/stubs/main.js +2 -0
  142. package/build/stubs/models/auth_user.stub +13 -0
  143. package/build/stubs/ui/edge/views/consent.edge +13 -0
  144. package/build/stubs/ui/edge/views/login.edge +19 -0
  145. package/build/stubs/ui/react/components/auth_shell.tsx +67 -0
  146. package/build/stubs/ui/react/pages/account/login.tsx +56 -0
  147. package/build/stubs/ui/react/pages/account/mfa.tsx +132 -0
  148. package/build/stubs/ui/react/pages/account/tokens.tsx +88 -0
  149. package/build/stubs/ui/react/pages/consent.tsx +39 -0
  150. package/build/stubs/ui/react/pages/forgot.tsx +44 -0
  151. package/build/stubs/ui/react/pages/login.tsx +171 -0
  152. package/build/stubs/ui/react/pages/mfa-challenge.tsx +72 -0
  153. package/build/stubs/ui/react/pages/reset.tsx +58 -0
  154. package/build/stubs/ui/react/pages/signup.tsx +78 -0
  155. package/build/stubs/ui/react/pages/verify-email.tsx +24 -0
  156. package/build/types.d.ts +7 -0
  157. package/build/types.js +1 -0
  158. package/package.json +108 -0
  159. package/stubs/config/authkit.stub +29 -0
  160. package/stubs/main.ts +2 -0
  161. package/stubs/models/auth_user.stub +13 -0
  162. package/stubs/ui/edge/views/consent.edge +13 -0
  163. package/stubs/ui/edge/views/login.edge +19 -0
  164. package/stubs/ui/react/components/auth_shell.tsx +67 -0
  165. package/stubs/ui/react/pages/account/login.tsx +56 -0
  166. package/stubs/ui/react/pages/account/mfa.tsx +132 -0
  167. package/stubs/ui/react/pages/account/tokens.tsx +88 -0
  168. package/stubs/ui/react/pages/consent.tsx +39 -0
  169. package/stubs/ui/react/pages/forgot.tsx +44 -0
  170. package/stubs/ui/react/pages/login.tsx +171 -0
  171. package/stubs/ui/react/pages/mfa-challenge.tsx +72 -0
  172. package/stubs/ui/react/pages/reset.tsx +58 -0
  173. package/stubs/ui/react/pages/signup.tsx +78 -0
  174. package/stubs/ui/react/pages/verify-email.tsx +24 -0
@@ -0,0 +1,169 @@
1
+ import '../augmentations.js';
2
+ import { brandFor } from '../branding.js';
3
+ import { signupValidator, forgotPasswordValidator, resetPasswordValidator } from '../validators.js';
4
+ import { sendPasswordResetEmail, sendEmailVerificationEmail } from '../default_mailer.js';
5
+ import { translate } from '../i18n.js';
6
+ export default class AuthRegistrationController {
7
+ /** GET /auth/interaction/:uid/signup — tela de cadastro (dentro do fluxo OIDC). */
8
+ async showSignup(ctx) {
9
+ const service = await ctx.containerResolver.make('authkit.server');
10
+ const cfg = service.config;
11
+ const render = cfg.render;
12
+ const details = await service.interactions.details(ctx);
13
+ const brand = brandFor(cfg.branding, details.params.client_id);
14
+ return render(ctx, 'signup', {
15
+ uid: details.uid,
16
+ csrfToken: ctx.request.csrfToken,
17
+ brand,
18
+ });
19
+ }
20
+ /** POST /auth/interaction/:uid/signup — cria o usuário e finaliza o login. */
21
+ async signup(ctx) {
22
+ const service = await ctx.containerResolver.make('authkit.server');
23
+ const cfg = service.config;
24
+ const render = cfg.render;
25
+ const details = await service.interactions.details(ctx);
26
+ const brand = brandFor(cfg.branding, details.params.client_id);
27
+ const data = await ctx.request.validateUsing(signupValidator);
28
+ const accountStore = cfg.accountStore;
29
+ const existing = await accountStore.findByEmail(data.email);
30
+ if (existing) {
31
+ return render(ctx, 'signup', {
32
+ uid: ctx.request.param('uid'),
33
+ csrfToken: ctx.request.csrfToken,
34
+ error: translate(cfg.messages, 'errors.email_taken'),
35
+ brand,
36
+ });
37
+ }
38
+ const created = await accountStore.create({
39
+ email: data.email,
40
+ password: data.password,
41
+ fullName: data.fullName,
42
+ });
43
+ await cfg.audit?.record({
44
+ type: 'signup',
45
+ accountId: created?.id ?? null,
46
+ email: data.email,
47
+ ip: ctx.request.ip?.() ?? null,
48
+ clientId: details.params.client_id ?? null,
49
+ });
50
+ // Finaliza a interaction como login (interactionFinished escreve o redirect 303 que
51
+ // retoma o authorize; o form nativo da tela segue esse redirect no sucesso).
52
+ const result = await service.interactions.login(ctx, {
53
+ email: data.email,
54
+ password: data.password,
55
+ });
56
+ if (!result.ok) {
57
+ return render(ctx, 'signup', {
58
+ uid: ctx.request.param('uid'),
59
+ csrfToken: ctx.request.csrfToken,
60
+ error: translate(cfg.messages, 'errors.signup_failed'),
61
+ brand,
62
+ });
63
+ }
64
+ // Verificação de e-mail (best-effort — não bloqueia nem reverte o login).
65
+ try {
66
+ const issued = await accountStore.issueEmailVerificationToken(data.email);
67
+ if (issued) {
68
+ await cfg.audit?.record({
69
+ type: 'email_verification.issued',
70
+ accountId: created?.id ?? null,
71
+ email: data.email,
72
+ ip: ctx.request.ip?.() ?? null,
73
+ });
74
+ const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
75
+ const verifyUrl = `${origin}/auth/verify-email?token=${issued.token}`;
76
+ // Hook do config tem prioridade (override); senão usa o mailer default do host.
77
+ if (cfg.mail?.onEmailVerification) {
78
+ await cfg.mail.onEmailVerification({ email: data.email, verifyUrl, token: issued.token });
79
+ }
80
+ else {
81
+ await sendEmailVerificationEmail(ctx, { email: data.email, verifyUrl });
82
+ }
83
+ }
84
+ }
85
+ catch (error) {
86
+ ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar verificação de e-mail');
87
+ }
88
+ }
89
+ /** GET /auth/forgot-password — tela standalone. */
90
+ async showForgot(ctx) {
91
+ const service = await ctx.containerResolver.make('authkit.server');
92
+ const cfg = service.config;
93
+ const render = cfg.render;
94
+ return render(ctx, 'forgot', { csrfToken: ctx.request.csrfToken });
95
+ }
96
+ /** POST /auth/forgot-password — gera token e (dev) loga o link. Sempre responde sucesso (não vaza emails). */
97
+ async forgot(ctx) {
98
+ const service = await ctx.containerResolver.make('authkit.server');
99
+ const cfg = service.config;
100
+ const render = cfg.render;
101
+ const { email } = await ctx.request.validateUsing(forgotPasswordValidator);
102
+ const accountStore = cfg.accountStore;
103
+ const result = await accountStore.issuePasswordResetToken(email);
104
+ if (result) {
105
+ await cfg.audit?.record({
106
+ type: 'password_reset.issued',
107
+ email,
108
+ ip: ctx.request.ip?.() ?? null,
109
+ });
110
+ const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
111
+ const url = `${origin}/auth/reset-password?token=${result.token}`;
112
+ // Hook do config tem prioridade (override); senão usa o mailer default do host.
113
+ if (cfg.mail?.onPasswordReset) {
114
+ await cfg.mail.onPasswordReset({ email, resetUrl: url, token: result.token });
115
+ }
116
+ else {
117
+ await sendPasswordResetEmail(ctx, { email, resetUrl: url });
118
+ }
119
+ }
120
+ return render(ctx, 'forgot', {
121
+ csrfToken: ctx.request.csrfToken,
122
+ sent: true,
123
+ });
124
+ }
125
+ /** GET /auth/reset-password?token=... — tela standalone. */
126
+ async showReset(ctx) {
127
+ const service = await ctx.containerResolver.make('authkit.server');
128
+ const cfg = service.config;
129
+ const render = cfg.render;
130
+ const token = ctx.request.qs().token ?? '';
131
+ return render(ctx, 'reset', { token, csrfToken: ctx.request.csrfToken });
132
+ }
133
+ /** POST /auth/reset-password — redefine a senha. */
134
+ async reset(ctx) {
135
+ const service = await ctx.containerResolver.make('authkit.server');
136
+ const cfg = service.config;
137
+ const render = cfg.render;
138
+ const { token, password } = await ctx.request.validateUsing(resetPasswordValidator);
139
+ const accountStore = cfg.accountStore;
140
+ const ok = await accountStore.consumePasswordResetToken(token, password);
141
+ if (!ok) {
142
+ return ctx.response.badRequest({ error: translate(cfg.messages, 'errors.invalid_or_expired_token') });
143
+ }
144
+ await cfg.audit?.record({
145
+ type: 'password_reset.consumed',
146
+ ip: ctx.request.ip?.() ?? null,
147
+ });
148
+ return render(ctx, 'reset', {
149
+ token: '',
150
+ csrfToken: ctx.request.csrfToken,
151
+ done: true,
152
+ });
153
+ }
154
+ /** GET /auth/verify-email?token=... — consome o token e mostra sucesso/falha. */
155
+ async verifyEmail(ctx) {
156
+ const service = await ctx.containerResolver.make('authkit.server');
157
+ const cfg = service.config;
158
+ const render = cfg.render;
159
+ const token = ctx.request.qs().token ?? '';
160
+ const ok = await cfg.accountStore.consumeEmailVerificationToken(token);
161
+ if (ok) {
162
+ await cfg.audit?.record({
163
+ type: 'email_verification.consumed',
164
+ ip: ctx.request.ip?.() ?? null,
165
+ });
166
+ }
167
+ return render(ctx, 'verify-email', { verified: ok });
168
+ }
169
+ }
@@ -0,0 +1,8 @@
1
+ import '../augmentations.js';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
+ export default class AuthSocialController {
4
+ /** GET /auth/:provider/redirect/:uid — guarda o uid e redireciona para o provider OAuth. */
5
+ redirect(ctx: HttpContext): Promise<void>;
6
+ /** GET /auth/:provider/callback — retorno do provider → acha/cria AuthUser → conclui a interaction. */
7
+ callback(ctx: HttpContext): Promise<void>;
8
+ }
@@ -0,0 +1,82 @@
1
+ import '../augmentations.js';
2
+ import { randomUUID } from 'node:crypto';
3
+ const UID_SESSION_KEY = 'authkit_social_uid';
4
+ /**
5
+ * `AllyService.use()` é tipado contra a interface `SocialProviders`, que só é
6
+ * preenchida pelo HOST (a lib não conhece os providers configurados). Para um
7
+ * nome de provider dinâmico (vindo da rota), o retorno colapsa em `never`, então
8
+ * resolvemos via o contrato genérico do driver — sem cast pro `HttpContext` inteiro.
9
+ */
10
+ function useProvider(ctx, provider) {
11
+ return ctx.ally.use(provider);
12
+ }
13
+ export default class AuthSocialController {
14
+ /** GET /auth/:provider/redirect/:uid — guarda o uid e redireciona para o provider OAuth. */
15
+ async redirect(ctx) {
16
+ const provider = ctx.request.param('provider');
17
+ ctx.session.put(UID_SESSION_KEY, ctx.request.param('uid'));
18
+ return useProvider(ctx, provider).redirect();
19
+ }
20
+ /** GET /auth/:provider/callback — retorno do provider → acha/cria AuthUser → conclui a interaction. */
21
+ async callback(ctx) {
22
+ const provider = ctx.request.param('provider');
23
+ const social = useProvider(ctx, provider);
24
+ const uid = ctx.session.get(UID_SESSION_KEY);
25
+ const backToLogin = uid ? `/auth/interaction/${uid}` : '/health';
26
+ if (social.accessDenied() || social.stateMisMatch() || social.hasError()) {
27
+ ctx.session.forget(UID_SESSION_KEY);
28
+ return ctx.response.redirect(backToLogin);
29
+ }
30
+ const profile = await social.user();
31
+ // `profile.id` é o id estável do usuário no provider — independe do e-mail.
32
+ if (!profile.id) {
33
+ ctx.session.forget(UID_SESSION_KEY);
34
+ return ctx.response.redirect(backToLogin);
35
+ }
36
+ const service = await ctx.containerResolver.make('authkit.server');
37
+ const cfg = service.config;
38
+ const store = cfg.accountStore;
39
+ const email = profile.email ?? undefined;
40
+ // Precedência do account linking:
41
+ // 1. Identidade de provider já ligada → loga essa conta.
42
+ // 2. Senão, e-mail conhecido → acha por e-mail e LIGA a identidade (linking).
43
+ // 3. Senão → cria conta nova e liga a identidade.
44
+ let user = await store.findByProviderIdentity(provider, profile.id);
45
+ if (!user && email) {
46
+ const byEmail = await store.findByEmail(email);
47
+ if (byEmail) {
48
+ await store.linkProviderIdentity({
49
+ accountId: byEmail.id,
50
+ provider,
51
+ providerUserId: profile.id,
52
+ email,
53
+ });
54
+ user = byEmail;
55
+ }
56
+ }
57
+ if (!user) {
58
+ // Sem e-mail não há como criar/identificar uma conta — volta ao login.
59
+ if (!email) {
60
+ ctx.session.forget(UID_SESSION_KEY);
61
+ return ctx.response.redirect(backToLogin);
62
+ }
63
+ const created = await store.create({
64
+ email,
65
+ password: randomUUID(),
66
+ fullName: profile.name ?? null,
67
+ emailVerified: true,
68
+ });
69
+ await store.linkProviderIdentity({
70
+ accountId: created.id,
71
+ provider,
72
+ providerUserId: profile.id,
73
+ email,
74
+ });
75
+ user = created;
76
+ }
77
+ ctx.session.forget(UID_SESSION_KEY);
78
+ // Conclui a interaction OIDC para este usuário (escreve o 303 de volta ao authorize).
79
+ // O cookie de interaction (path '/') sobrevive ao round-trip do provider OAuth.
80
+ await service.interactions.completeLogin(ctx, user.id);
81
+ }
82
+ }
@@ -0,0 +1,39 @@
1
+ import type { HttpContext } from '@adonisjs/core/http';
2
+ /**
3
+ * Envio de e-mail default do host-kit, usando o mailer `default` do host via
4
+ * `@adonisjs/mail`. Permite que o dev não escreva nenhum hook: por padrão os
5
+ * e-mails de reset de senha / verificação são enviados pelo mailer já configurado
6
+ * no app, com HTML responsivo + branding (do `config/authkit.ts`) e fallback texto.
7
+ * Os hooks de `config/authkit.ts` (`mail.onPasswordReset` /
8
+ * `mail.onEmailVerification`), quando presentes, têm prioridade (override).
9
+ *
10
+ * Best-effort: se `@adonisjs/mail` não estiver instalado/configurado, cai no
11
+ * fallback de log (mesmo comportamento de antes) e NUNCA lança na request.
12
+ */
13
+ /**
14
+ * Service do `@adonisjs/mail` resolvido de forma preguiçosa. Tipado como `any` de
15
+ * propósito: a lib NÃO depende do mail em tempo de compilação (peer/opt-in).
16
+ */
17
+ type MailService = any;
18
+ /**
19
+ * Permite reapontar/limpar o loader do mail (usado em testes).
20
+ * @internal
21
+ */
22
+ export declare function __setMailLoaderForTests(fn: (() => Promise<MailService | null>) | undefined): void;
23
+ /**
24
+ * Envia o e-mail de redefinição de senha pelo mailer default do host.
25
+ * Best-effort: no fallback (sem mail) loga o link; nunca lança.
26
+ */
27
+ export declare function sendPasswordResetEmail(ctx: HttpContext, data: {
28
+ email: string;
29
+ resetUrl: string;
30
+ }): Promise<void>;
31
+ /**
32
+ * Envia o e-mail de verificação pelo mailer default do host.
33
+ * Best-effort: no fallback (sem mail) loga o link; nunca lança.
34
+ */
35
+ export declare function sendEmailVerificationEmail(ctx: HttpContext, data: {
36
+ email: string;
37
+ verifyUrl: string;
38
+ }): Promise<void>;
39
+ export {};
@@ -0,0 +1,141 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { renderTransactionalEmail } from './email_templates.js';
10
+ let mailServicePromise;
11
+ /**
12
+ * Importa o service de mail do HOST de forma preguiçosa e fail-safe.
13
+ * Se `@adonisjs/mail` não estiver instalado, resolve `null`.
14
+ */
15
+ async function loadMail() {
16
+ if (!mailServicePromise) {
17
+ // Indireção via variável: o `@adonisjs/mail` é peer/opcional e pode não estar
18
+ // instalado na lib, então o specifier não é resolvido em build-time.
19
+ const specifier = '@adonisjs/mail/services/main';
20
+ mailServicePromise = import(__rewriteRelativeImportExtension(specifier))
21
+ .then((mod) => mod.default ?? null)
22
+ .catch(() => null);
23
+ }
24
+ return mailServicePromise;
25
+ }
26
+ /**
27
+ * Permite reapontar/limpar o loader do mail (usado em testes).
28
+ * @internal
29
+ */
30
+ export function __setMailLoaderForTests(fn) {
31
+ if (fn) {
32
+ mailServicePromise = fn();
33
+ }
34
+ else {
35
+ mailServicePromise = undefined;
36
+ }
37
+ }
38
+ /**
39
+ * Tenta descobrir o `from` default da config de mail do host. Se não houver,
40
+ * retorna `undefined` (deixa o @adonisjs/mail aplicar o default da própria config).
41
+ */
42
+ function defaultFrom(ctx) {
43
+ try {
44
+ // Alcançamos a config de `mail` do HOST via o resolver do container. Esse
45
+ // formato é específico do app (a lib não conhece o shape da config de mail
46
+ // nem `containerResolver.app` é público), então tipamos apenas ESTE acesso
47
+ // como `unknown`/cast estreito — não o `HttpContext` inteiro.
48
+ const resolver = ctx.containerResolver;
49
+ const cfg = resolver.app?.config?.get?.('mail');
50
+ const from = cfg?.from;
51
+ if (from)
52
+ return from;
53
+ }
54
+ catch {
55
+ // sem config de mail resolvível — deixa o @adonisjs/mail decidir.
56
+ }
57
+ return undefined;
58
+ }
59
+ /**
60
+ * Resolve a marca default a partir do `config/authkit.ts` (branding.default +
61
+ * company). Usada para o cabeçalho/cor de acento/rodapé dos e-mails. Cai num
62
+ * default neutro se a config não for resolvível.
63
+ */
64
+ function resolveBrand(ctx) {
65
+ try {
66
+ const resolver = ctx.containerResolver;
67
+ const branding = resolver.app?.config?.get?.('authkit')?.branding;
68
+ if (branding) {
69
+ return {
70
+ appName: branding.default?.appName || branding.company || 'AuthKit',
71
+ accent: branding.default?.accent,
72
+ company: branding.company,
73
+ };
74
+ }
75
+ }
76
+ catch {
77
+ // sem config authkit resolvível — usa default neutro.
78
+ }
79
+ return { appName: 'AuthKit' };
80
+ }
81
+ async function sendEmail(ctx, to, content) {
82
+ const mail = await loadMail();
83
+ if (!mail)
84
+ return false;
85
+ const from = defaultFrom(ctx);
86
+ await mail.send((message) => {
87
+ if (from)
88
+ message.from(from);
89
+ message.to(to).subject(content.subject).html(content.html).text(content.text);
90
+ });
91
+ return true;
92
+ }
93
+ /**
94
+ * Envia o e-mail de redefinição de senha pelo mailer default do host.
95
+ * Best-effort: no fallback (sem mail) loga o link; nunca lança.
96
+ */
97
+ export async function sendPasswordResetEmail(ctx, data) {
98
+ try {
99
+ const brand = resolveBrand(ctx);
100
+ const content = renderTransactionalEmail({
101
+ brand,
102
+ subject: 'Redefinição de senha',
103
+ heading: 'Redefinição de senha',
104
+ intro: `Recebemos um pedido para redefinir a senha da sua conta em ${brand.appName}. Clique no botão abaixo para escolher uma nova senha. Se não foi você, ignore este e-mail.`,
105
+ ctaLabel: 'Redefinir senha',
106
+ ctaUrl: data.resetUrl,
107
+ footnote: 'Por segurança, este link expira em breve e só pode ser usado uma vez.',
108
+ });
109
+ const sent = await sendEmail(ctx, data.email, content);
110
+ if (!sent) {
111
+ ctx.logger.info({ resetUrl: data.resetUrl, email: data.email }, 'authkit: link de redefinição de senha (dev — @adonisjs/mail ausente)');
112
+ }
113
+ }
114
+ catch (error) {
115
+ ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar e-mail de redefinição de senha');
116
+ }
117
+ }
118
+ /**
119
+ * Envia o e-mail de verificação pelo mailer default do host.
120
+ * Best-effort: no fallback (sem mail) loga o link; nunca lança.
121
+ */
122
+ export async function sendEmailVerificationEmail(ctx, data) {
123
+ try {
124
+ const brand = resolveBrand(ctx);
125
+ const content = renderTransactionalEmail({
126
+ brand,
127
+ subject: 'Verifique seu e-mail',
128
+ heading: 'Confirme seu e-mail',
129
+ intro: `Bem-vindo(a) ao ${brand.appName}! Confirme seu endereço de e-mail clicando no botão abaixo para ativar sua conta.`,
130
+ ctaLabel: 'Verificar e-mail',
131
+ ctaUrl: data.verifyUrl,
132
+ });
133
+ const sent = await sendEmail(ctx, data.email, content);
134
+ if (!sent) {
135
+ ctx.logger.info({ verifyUrl: data.verifyUrl, email: data.email }, 'authkit: link de verificação de e-mail (dev — @adonisjs/mail ausente)');
136
+ }
137
+ }
138
+ catch (error) {
139
+ ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar verificação de e-mail');
140
+ }
141
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Renderização de e-mails transacionais do host-kit (reset de senha / verificação).
3
+ *
4
+ * Sem dependências (template literals) e com HTML email-safe: estilos inline,
5
+ * layout em tabela centralizada, botão de CTA com a cor de acento da marca, e
6
+ * sempre um corpo `text` de fallback. Branding vem do `config/authkit.ts`
7
+ * (`branding.default` ou a marca do client). Tudo escapado para evitar injeção.
8
+ */
9
+ export interface EmailContent {
10
+ subject: string;
11
+ html: string;
12
+ text: string;
13
+ }
14
+ interface EmailTemplateInput {
15
+ /** Marca usada no cabeçalho/botão/rodapé (accent/company opcionais). */
16
+ brand: {
17
+ appName: string;
18
+ accent?: string;
19
+ company?: string;
20
+ };
21
+ /** Assunto do e-mail. */
22
+ subject: string;
23
+ /** Saudação/título dentro do card. */
24
+ heading: string;
25
+ /** Parágrafo de introdução (texto puro, será escapado). */
26
+ intro: string;
27
+ /** Rótulo do botão de CTA. */
28
+ ctaLabel: string;
29
+ /** URL do CTA. */
30
+ ctaUrl: string;
31
+ /** Linha auxiliar abaixo do botão (ex.: validade do link). */
32
+ footnote?: string;
33
+ }
34
+ export declare function renderTransactionalEmail(input: EmailTemplateInput): EmailContent;
35
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Renderização de e-mails transacionais do host-kit (reset de senha / verificação).
3
+ *
4
+ * Sem dependências (template literals) e com HTML email-safe: estilos inline,
5
+ * layout em tabela centralizada, botão de CTA com a cor de acento da marca, e
6
+ * sempre um corpo `text` de fallback. Branding vem do `config/authkit.ts`
7
+ * (`branding.default` ou a marca do client). Tudo escapado para evitar injeção.
8
+ */
9
+ /** Escapa texto para interpolação segura em HTML. */
10
+ function esc(value) {
11
+ return value
12
+ .replace(/&/g, '&amp;')
13
+ .replace(/</g, '&lt;')
14
+ .replace(/>/g, '&gt;')
15
+ .replace(/"/g, '&quot;')
16
+ .replace(/'/g, '&#39;');
17
+ }
18
+ const FALLBACK_ACCENT = '#4f46e5';
19
+ export function renderTransactionalEmail(input) {
20
+ const appName = input.brand.appName || input.brand.company || 'AuthKit';
21
+ const accent = input.brand.accent || FALLBACK_ACCENT;
22
+ const company = input.brand.company || appName;
23
+ const year = '©'; // ano resolvido fora (sem Date.* aqui); rodapé usa só o nome.
24
+ const html = `<!doctype html>
25
+ <html lang="pt-BR">
26
+ <head>
27
+ <meta charset="utf-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1">
29
+ <title>${esc(input.subject)}</title>
30
+ </head>
31
+ <body style="margin:0;padding:0;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;">
32
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;padding:32px 12px;">
33
+ <tr><td align="center">
34
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:480px;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.08);">
35
+ <tr><td style="background:${esc(accent)};padding:20px 28px;">
36
+ <span style="color:#ffffff;font-size:18px;font-weight:700;letter-spacing:.2px;">${esc(appName)}</span>
37
+ </td></tr>
38
+ <tr><td style="padding:32px 28px 8px;">
39
+ <h1 style="margin:0 0 12px;font-size:20px;line-height:1.3;color:#111827;">${esc(input.heading)}</h1>
40
+ <p style="margin:0 0 24px;font-size:15px;line-height:1.6;color:#374151;">${esc(input.intro)}</p>
41
+ <table role="presentation" cellpadding="0" cellspacing="0"><tr><td style="border-radius:8px;background:${esc(accent)};">
42
+ <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>
43
+ </td></tr></table>
44
+ ${input.footnote ? `<p style="margin:24px 0 0;font-size:13px;line-height:1.5;color:#6b7280;">${esc(input.footnote)}</p>` : ''}
45
+ <p style="margin:24px 0 0;font-size:13px;line-height:1.5;color:#6b7280;">Se o botão não funcionar, copie e cole este link no navegador:<br><a href="${esc(input.ctaUrl)}" style="color:${esc(accent)};word-break:break-all;">${esc(input.ctaUrl)}</a></p>
46
+ </td></tr>
47
+ <tr><td style="padding:24px 28px 28px;border-top:1px solid #f3f4f6;">
48
+ <p style="margin:0;font-size:12px;line-height:1.5;color:#9ca3af;">${esc(company)} ${year}</p>
49
+ </td></tr>
50
+ </table>
51
+ </td></tr>
52
+ </table>
53
+ </body>
54
+ </html>`;
55
+ const text = [
56
+ input.heading,
57
+ '',
58
+ input.intro,
59
+ '',
60
+ `${input.ctaLabel}: ${input.ctaUrl}`,
61
+ ...(input.footnote ? ['', input.footnote] : []),
62
+ '',
63
+ `— ${company}`,
64
+ ].join('\n');
65
+ return { subject: input.subject, html, text };
66
+ }