@adonis-agora/authkit-server 0.47.0 → 0.48.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 (188) hide show
  1. package/build/commands/clients_create.js +4 -4
  2. package/build/commands/configure.js +3 -2
  3. package/build/commands/doctor.js +8 -5
  4. package/build/commands/eject.js +1 -1
  5. package/build/commands/expire_scan.js +7 -5
  6. package/build/commands/import_users.js +1 -1
  7. package/build/commands/keys_rotate.js +7 -4
  8. package/build/host/views/account/apps.edge +5 -5
  9. package/build/host/views/account/login.edge +1 -1
  10. package/build/host/views/account/mfa.edge +7 -7
  11. package/build/host/views/account/orgs.edge +5 -5
  12. package/build/host/views/account/security.edge +10 -10
  13. package/build/host/views/account/tokens.edge +5 -5
  14. package/build/index.d.ts +7 -7
  15. package/build/index.js +5 -5
  16. package/build/providers/authkit_server_provider.d.ts +2 -2
  17. package/build/providers/authkit_server_provider.js +20 -11
  18. package/build/services/main.d.ts +1 -1
  19. package/build/services/main.js +2 -2
  20. package/build/src/accounts/lucid_account_store.d.ts +3 -3
  21. package/build/src/accounts/lucid_account_store.js +6 -8
  22. package/build/src/accounts/lucid_store/core.js +1 -1
  23. package/build/src/accounts/lucid_store/mfa.js +1 -1
  24. package/build/src/accounts/lucid_store/organizations.js +2 -4
  25. package/build/src/accounts/lucid_store/password_hygiene.js +1 -4
  26. package/build/src/accounts/lucid_store/shared.d.ts +1 -1
  27. package/build/src/accounts/lucid_stores.d.ts +3 -3
  28. package/build/src/accounts/lucid_stores.js +2 -2
  29. package/build/src/adapters/database_adapter.js +3 -1
  30. package/build/src/adapters/factory.js +1 -1
  31. package/build/src/adapters/redis_adapter.js +2 -1
  32. package/build/src/commands/expire_scan_command.js +19 -5
  33. package/build/src/commands/settings_commands.js +22 -5
  34. package/build/src/define_config.d.ts +20 -20
  35. package/build/src/define_config.js +62 -67
  36. package/build/src/doctor/checks.js +45 -20
  37. package/build/src/events/dispatcher.d.ts +1 -1
  38. package/build/src/events/dispatcher.js +10 -10
  39. package/build/src/host/account_api/account_api_controller.js +95 -39
  40. package/build/src/host/account_deletion_ops.d.ts +4 -4
  41. package/build/src/host/account_deletion_ops.js +5 -5
  42. package/build/src/host/account_deletion_service.d.ts +2 -2
  43. package/build/src/host/account_deletion_service.js +2 -2
  44. package/build/src/host/account_export_service.d.ts +1 -1
  45. package/build/src/host/account_export_service.js +4 -6
  46. package/build/src/host/account_home.d.ts +0 -8
  47. package/build/src/host/account_home.js +6 -4
  48. package/build/src/host/account_login_url.d.ts +6 -3
  49. package/build/src/host/account_login_url.js +16 -8
  50. package/build/src/host/account_paths.d.ts +120 -0
  51. package/build/src/host/account_paths.js +161 -0
  52. package/build/src/host/admin_api/admin_orgs_service.d.ts +2 -2
  53. package/build/src/host/admin_api/admin_orgs_service.js +3 -2
  54. package/build/src/host/admin_api/admin_users_service.d.ts +12 -12
  55. package/build/src/host/admin_api/admin_users_service.js +20 -20
  56. package/build/src/host/admin_api/api_clients_controller.js +2 -2
  57. package/build/src/host/admin_api/api_misc_controller.js +2 -2
  58. package/build/src/host/admin_api/api_orgs_controller.js +3 -3
  59. package/build/src/host/admin_api/api_settings_controller.js +11 -5
  60. package/build/src/host/admin_api/api_users_controller.d.ts +2 -2
  61. package/build/src/host/admin_api/api_users_controller.js +37 -38
  62. package/build/src/host/admin_api/dto.d.ts +4 -4
  63. package/build/src/host/admin_clients_service.js +5 -4
  64. package/build/src/host/admin_console/admin_shell_controller.js +4 -2
  65. package/build/src/host/admin_console/console_audit_controller.js +1 -1
  66. package/build/src/host/admin_console/console_clients_controller.js +5 -3
  67. package/build/src/host/admin_console/console_impersonation_controller.js +1 -1
  68. package/build/src/host/admin_console/console_keys_controller.js +2 -4
  69. package/build/src/host/admin_console/console_orgs_controller.js +3 -3
  70. package/build/src/host/admin_console/console_roles_controller.js +30 -12
  71. package/build/src/host/admin_console/console_sessions_controller.js +3 -3
  72. package/build/src/host/admin_console/console_settings_controller.js +11 -5
  73. package/build/src/host/admin_console/console_users_controller.d.ts +2 -2
  74. package/build/src/host/admin_console/console_users_controller.js +46 -47
  75. package/build/src/host/admin_prefix.js +2 -2
  76. package/build/src/host/admin_sessions_service.js +1 -3
  77. package/build/src/host/adonis_auth_sync.d.ts +1 -1
  78. package/build/src/host/adonis_auth_user_provider.d.ts +1 -1
  79. package/build/src/host/app_key.js +1 -1
  80. package/build/src/host/avatar_storage.js +1 -3
  81. package/build/src/host/config_locks.js +1 -3
  82. package/build/src/host/console_session.js +1 -1
  83. package/build/src/host/controllers/account_apps_controller.js +3 -2
  84. package/build/src/host/controllers/account_confirm_controller.js +2 -6
  85. package/build/src/host/controllers/account_mfa_controller.js +63 -12
  86. package/build/src/host/controllers/account_orgs_controller.js +47 -27
  87. package/build/src/host/controllers/account_security_controller.d.ts +2 -2
  88. package/build/src/host/controllers/account_security_controller.js +114 -115
  89. package/build/src/host/controllers/account_session_controller.js +5 -5
  90. package/build/src/host/controllers/account_tokens_controller.js +3 -2
  91. package/build/src/host/controllers/interaction_controller.js +59 -22
  92. package/build/src/host/controllers/registration_controller.js +28 -13
  93. package/build/src/host/default_mailer.js +11 -7
  94. package/build/src/host/durable/account_deletion_workflow.d.ts +2 -2
  95. package/build/src/host/durable/account_deletion_workflow.js +14 -14
  96. package/build/src/host/durable/account_export_workflow.d.ts +2 -2
  97. package/build/src/host/durable/account_export_workflow.js +12 -12
  98. package/build/src/host/durable/index.d.ts +5 -5
  99. package/build/src/host/durable/index.js +6 -6
  100. package/build/src/host/i18n.d.ts +1376 -1376
  101. package/build/src/host/i18n.js +1380 -1380
  102. package/build/src/host/impersonation.js +1 -7
  103. package/build/src/host/key_rotation_actions.d.ts +1 -1
  104. package/build/src/host/login_attempt.js +35 -6
  105. package/build/src/host/login_notify.js +1 -1
  106. package/build/src/host/otp_lockout.js +1 -3
  107. package/build/src/host/rate_limit.js +1 -1
  108. package/build/src/host/register_auth_host.d.ts +30 -0
  109. package/build/src/host/register_auth_host.js +120 -74
  110. package/build/src/host/renderers/edge_renderer.js +15 -2
  111. package/build/src/host/renderers/inertia_renderer.d.ts +9 -1
  112. package/build/src/host/renderers/inertia_renderer.js +9 -1
  113. package/build/src/host/runtime_settings.js +1 -1
  114. package/build/src/host/runtime_toggles.d.ts +3 -3
  115. package/build/src/host/runtime_toggles.js +69 -24
  116. package/build/src/host/security_notice_service.d.ts +1 -1
  117. package/build/src/host/security_notice_service.js +1 -1
  118. package/build/src/host/session_context.js +1 -1
  119. package/build/src/host/sudo/index.d.ts +3 -3
  120. package/build/src/host/sudo/index.js +3 -3
  121. package/build/src/host/sudo/methods/magic_link.js +6 -5
  122. package/build/src/host/sudo/methods/passkey.js +7 -4
  123. package/build/src/host/sudo/methods/password.js +8 -3
  124. package/build/src/host/sudo/runtime.js +6 -13
  125. package/build/src/host/sudo_mode.js +3 -2
  126. package/build/src/host/ui-dist/assets/index-BQoTDiLV.js +137 -0
  127. package/build/src/host/ui-dist/index.html +1 -1
  128. package/build/src/keys/jwks_manager.js +1 -1
  129. package/build/src/keys/keystore.js +1 -1
  130. package/build/src/keys/keystore_manager.d.ts +2 -2
  131. package/build/src/keys/keystore_manager.js +2 -2
  132. package/build/src/keys/keystore_vault.js +8 -4
  133. package/build/src/mixins/with_audit_log.d.ts +1 -1
  134. package/build/src/mixins/with_auth_user.d.ts +1 -1
  135. package/build/src/mixins/with_auth_user.js +1 -1
  136. package/build/src/mixins/with_credentials.d.ts +1 -1
  137. package/build/src/mixins/with_mfa.d.ts +1 -1
  138. package/build/src/mixins/with_personal_access_token.d.ts +1 -1
  139. package/build/src/mixins/with_provider_identity.d.ts +1 -1
  140. package/build/src/mixins/with_webauthn_credential.d.ts +1 -1
  141. package/build/src/observability/diagnostics_bridge.js +2 -2
  142. package/build/src/observability/metrics_service.js +1 -1
  143. package/build/src/observability/telescope/data_providers.d.ts +1 -1
  144. package/build/src/observability/telescope/data_providers.js +29 -43
  145. package/build/src/observability/telescope/extension.d.ts +1 -1
  146. package/build/src/observability/telescope/extension.js +58 -58
  147. package/build/src/observability/telescope/index.d.ts +2 -2
  148. package/build/src/observability/telescope/index.js +2 -2
  149. package/build/src/observability/wire_provider_events.d.ts +1 -1
  150. package/build/src/observability/wire_provider_events.js +11 -11
  151. package/build/src/password/common_passwords.js +1 -1
  152. package/build/src/password/password_manager.d.ts +1 -1
  153. package/build/src/password/password_manager.js +4 -4
  154. package/build/src/pat/lucid_pat_store.js +2 -6
  155. package/build/src/pat/pat_tokens.js +1 -1
  156. package/build/src/provider/interaction_actions.js +1 -1
  157. package/build/src/provider/key_rotation_scheduler.js +3 -1
  158. package/build/src/provider/logout_sources.js +2 -8
  159. package/build/src/provider/oidc_service.d.ts +2 -2
  160. package/build/src/provider/oidc_service.js +22 -14
  161. package/build/src/provider/single_flight_lock.js +1 -3
  162. package/build/src/provider/token_exchange.d.ts +1 -1
  163. package/build/src/schema/ensure.js +3 -1
  164. package/build/stubs/ui/react/components/auth_shell.tsx +19 -19
  165. package/build/stubs/ui/react/pages/account/login.tsx +6 -7
  166. package/build/stubs/ui/react/pages/account/mfa.tsx +16 -15
  167. package/build/stubs/ui/react/pages/account/tokens.tsx +27 -17
  168. package/build/stubs/ui/react/pages/consent.tsx +12 -12
  169. package/build/stubs/ui/react/pages/forgot.tsx +7 -7
  170. package/build/stubs/ui/react/pages/login.tsx +22 -24
  171. package/build/stubs/ui/react/pages/mfa-challenge.tsx +9 -10
  172. package/build/stubs/ui/react/pages/reset.tsx +10 -10
  173. package/build/stubs/ui/react/pages/signup.tsx +30 -16
  174. package/build/stubs/ui/react/pages/verify-email.tsx +6 -6
  175. package/package.json +1 -1
  176. package/stubs/main.ts +2 -2
  177. package/stubs/ui/react/components/auth_shell.tsx +19 -19
  178. package/stubs/ui/react/pages/account/login.tsx +6 -7
  179. package/stubs/ui/react/pages/account/mfa.tsx +16 -15
  180. package/stubs/ui/react/pages/account/tokens.tsx +27 -17
  181. package/stubs/ui/react/pages/consent.tsx +12 -12
  182. package/stubs/ui/react/pages/forgot.tsx +7 -7
  183. package/stubs/ui/react/pages/login.tsx +22 -24
  184. package/stubs/ui/react/pages/mfa-challenge.tsx +9 -10
  185. package/stubs/ui/react/pages/reset.tsx +10 -10
  186. package/stubs/ui/react/pages/signup.tsx +30 -16
  187. package/stubs/ui/react/pages/verify-email.tsx +6 -6
  188. package/build/src/host/ui-dist/assets/index-BmI_J87g.js +0 -137
@@ -1,13 +1,29 @@
1
1
  import '../augmentations.js';
2
2
  import QRCode from 'qrcode';
3
- import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
4
- import { translate } from '../i18n.js';
5
3
  import { supportsPasskeys } from '../../accounts/account_store.js';
6
- import { dispatchSecurityNotice } from '../security_notice_service.js';
4
+ import { accountPath } from '../account_paths.js';
5
+ import { translate } from '../i18n.js';
6
+ import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
7
7
  import { resolveRuntimeSettings } from '../runtime_settings.js';
8
+ import { dispatchSecurityNotice } from '../security_notice_service.js';
8
9
  import { requireSudo } from '../sudo_mode.js';
9
10
  /** Desafio WebAuthn pendente (registro) guardado na sessão entre begin/finish. */
10
11
  const PASSKEY_REG_CHALLENGE_KEY = 'authkit_passkey_reg_challenge';
12
+ /**
13
+ * `true` quando a requisição é uma NAVEGAÇÃO (form/browser) e não um XHR/fetch:
14
+ * o cliente aceita `text/html` e NÃO pede explicitamente `application/json`.
15
+ *
16
+ * Endpoints duais (JSON para o app React / redirect para o `<form>` clássico)
17
+ * decidem a resposta por aqui. Um `fetch` sem `Accept` explícito manda coringa
18
+ * (`*` + `/` + `*`) → não é navegação → JSON (mantém o comportamento de sempre
19
+ * da `mfa.edge`).
20
+ */
21
+ function isNavigationRequest(ctx) {
22
+ const accept = (ctx.request.header('accept') ?? '').toLowerCase();
23
+ const wantsJson = accept.includes('application/json');
24
+ const wantsHtml = accept.includes('text/html');
25
+ return wantsHtml && !wantsJson;
26
+ }
11
27
  /**
12
28
  * Console de MFA da conta (atrás do account_auth middleware). Enrollment TOTP
13
29
  * com QR, confirmação com código, exibição única dos recovery codes e disable.
@@ -90,8 +106,28 @@ export default class AccountMfaController {
90
106
  if (accountForNotice) {
91
107
  const ts = new Date().toISOString();
92
108
  const ip = ctx.request.ip?.() ?? null;
93
- await dispatchSecurityNotice(ctx, { account: { id: userId, email: accountForNotice.email }, kind: 'passkey_added', ip, timestamp: ts }, cfg.mail, cfg.audit);
94
- await dispatchSecurityNotice(ctx, { account: { id: userId, email: accountForNotice.email }, kind: 'mfa_enabled', ip, timestamp: ts }, cfg.mail, cfg.audit);
109
+ await dispatchSecurityNotice(ctx, {
110
+ account: { id: userId, email: accountForNotice.email },
111
+ kind: 'passkey_added',
112
+ ip,
113
+ timestamp: ts,
114
+ }, cfg.mail, cfg.audit);
115
+ await dispatchSecurityNotice(ctx, {
116
+ account: { id: userId, email: accountForNotice.email },
117
+ kind: 'mfa_enabled',
118
+ ip,
119
+ timestamp: ts,
120
+ }, cfg.mail, cfg.audit);
121
+ }
122
+ // Resposta DUAL. A cerimônia WebAuthn da tela built-in (`mfa.edge`) chama
123
+ // este endpoint por `fetch` e só olha `res.ok` — mas um `<form>` HTML
124
+ // clássico (progressive enhancement, ou host que POSTa a assertion sem JS)
125
+ // fica encarando o JSON `{ok:true}` cru. Navegação (o cliente ACEITA HTML e
126
+ // NÃO pede JSON) → redirect para a tela de MFA (respeitando `accountRoutes`);
127
+ // XHR/fetch → JSON de sempre. O fetch da view manda Accept coringa, então
128
+ // cai no ramo JSON e continua funcionando.
129
+ if (isNavigationRequest(ctx)) {
130
+ return ctx.response.redirect(accountPath('mfa'));
95
131
  }
96
132
  return { ok: true };
97
133
  }
@@ -116,9 +152,14 @@ export default class AccountMfaController {
116
152
  // Notificação de segurança: passkey_removed (best-effort).
117
153
  const accountForNotice = await cfg.accountStore.findById(userId);
118
154
  if (accountForNotice) {
119
- await dispatchSecurityNotice(ctx, { account: { id: userId, email: accountForNotice.email }, kind: 'passkey_removed', ip: ctx.request.ip?.() ?? null, timestamp: new Date().toISOString() }, cfg.mail, cfg.audit);
155
+ await dispatchSecurityNotice(ctx, {
156
+ account: { id: userId, email: accountForNotice.email },
157
+ kind: 'passkey_removed',
158
+ ip: ctx.request.ip?.() ?? null,
159
+ timestamp: new Date().toISOString(),
160
+ }, cfg.mail, cfg.audit);
120
161
  }
121
- return ctx.response.redirect('/account/mfa');
162
+ return ctx.response.redirect(accountPath('mfa'));
122
163
  }
123
164
  /** POST /account/mfa/enroll — gera segredo pendente + QR e mostra a confirmação. */
124
165
  async enroll(ctx) {
@@ -133,7 +174,7 @@ export default class AccountMfaController {
133
174
  return sudoResultEnroll;
134
175
  const started = await cfg.accountStore.startTotpEnrollment?.(userId);
135
176
  if (!started) {
136
- return ctx.response.redirect('/account/mfa');
177
+ return ctx.response.redirect(accountPath('mfa'));
137
178
  }
138
179
  // QR renderizado server-side como data-URL e passado como prop.
139
180
  const qrDataUrl = await QRCode.toDataURL(started.otpauthUri);
@@ -176,11 +217,16 @@ export default class AccountMfaController {
176
217
  // Notificação de segurança: mfa_enabled (best-effort).
177
218
  const accountForMfaNotice = await cfg.accountStore.findById(userId);
178
219
  if (accountForMfaNotice) {
179
- await dispatchSecurityNotice(ctx, { account: { id: userId, email: accountForMfaNotice.email }, kind: 'mfa_enabled', ip: ctx.request.ip?.() ?? null, timestamp: new Date().toISOString() }, cfg.mail, cfg.audit);
220
+ await dispatchSecurityNotice(ctx, {
221
+ account: { id: userId, email: accountForMfaNotice.email },
222
+ kind: 'mfa_enabled',
223
+ ip: ctx.request.ip?.() ?? null,
224
+ timestamp: new Date().toISOString(),
225
+ }, cfg.mail, cfg.audit);
180
226
  }
181
227
  // Mostra os recovery codes UMA vez (flash) e volta pro estado "ativado".
182
228
  ctx.session.flash('recoveryCodes', result.recoveryCodes ?? []);
183
- return ctx.response.redirect('/account/mfa');
229
+ return ctx.response.redirect(accountPath('mfa'));
184
230
  }
185
231
  /** POST /account/mfa/disable — desliga o MFA. */
186
232
  async disable(ctx) {
@@ -201,8 +247,13 @@ export default class AccountMfaController {
201
247
  // Notificação de segurança: mfa_disabled (best-effort).
202
248
  const accountForDisableNotice = await cfg.accountStore.findById(userId);
203
249
  if (accountForDisableNotice) {
204
- await dispatchSecurityNotice(ctx, { account: { id: userId, email: accountForDisableNotice.email }, kind: 'mfa_disabled', ip: ctx.request.ip?.() ?? null, timestamp: new Date().toISOString() }, cfg.mail, cfg.audit);
250
+ await dispatchSecurityNotice(ctx, {
251
+ account: { id: userId, email: accountForDisableNotice.email },
252
+ kind: 'mfa_disabled',
253
+ ip: ctx.request.ip?.() ?? null,
254
+ timestamp: new Date().toISOString(),
255
+ }, cfg.mail, cfg.audit);
205
256
  }
206
- return ctx.response.redirect('/account/mfa');
257
+ return ctx.response.redirect(accountPath('mfa'));
207
258
  }
208
259
  }
@@ -1,10 +1,11 @@
1
1
  import '../augmentations.js';
2
2
  import { supportsOrganizations } from '../../accounts/account_store.js';
3
- import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
4
3
  import { getAccountLoginUrl } from '../account_login_url.js';
4
+ import { accountPath } from '../account_paths.js';
5
+ import { ACTIVE_ORG_COOKIE, ACTIVE_ORG_COOKIE_TTL, encodeActiveOrgCookie, } from '../active_org_cookie.js';
6
+ import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
5
7
  import { resolveRuntimeSettings } from '../runtime_settings.js';
6
8
  import { isRoleInCatalog } from '../runtime_toggles.js';
7
- import { ACTIVE_ORG_COOKIE, ACTIVE_ORG_COOKIE_TTL, encodeActiveOrgCookie, } from '../active_org_cookie.js';
8
9
  /**
9
10
  * Console de conta — Organizations. Server-rendered, padrão dos outros controllers
10
11
  * de conta (account_tokens_controller, account_security_controller, etc.).
@@ -52,9 +53,7 @@ export default class AccountOrgsController {
52
53
  availableRoles: cfg.organizations.roles,
53
54
  messages,
54
55
  };
55
- return cfg.render
56
- ? cfg.render(ctx, 'account/orgs', props)
57
- : response.notFound();
56
+ return cfg.render ? cfg.render(ctx, 'account/orgs', props) : response.notFound();
58
57
  }
59
58
  /** POST /account/orgs — criar nova org (requer allowSelfCreate). */
60
59
  async store(ctx) {
@@ -69,7 +68,7 @@ export default class AccountOrgsController {
69
68
  const name = request.input('name', '').trim();
70
69
  const slug = request.input('slug', '').trim();
71
70
  if (!name || !slug)
72
- return response.redirect('/account/orgs');
71
+ return response.redirect(accountPath('orgs'));
73
72
  try {
74
73
  await store.createOrg({ name, slug, ownerAccountId: accountId });
75
74
  await cfg.audit?.record({ type: 'organization.created', accountId, metadata: { slug } });
@@ -77,7 +76,7 @@ export default class AccountOrgsController {
77
76
  catch {
78
77
  // slug duplicado ou outro erro — redireciona sem mensagem de erro específica
79
78
  }
80
- return response.redirect('/account/orgs');
79
+ return response.redirect(accountPath('orgs'));
81
80
  }
82
81
  /** POST /account/orgs/:id/activate — define org ativa (valida membership). */
83
82
  async activate(ctx) {
@@ -91,12 +90,16 @@ export default class AccountOrgsController {
91
90
  const orgId = params.id;
92
91
  const membership = await store.getOrgMembership(orgId, accountId);
93
92
  if (!membership)
94
- return response.redirect('/account/orgs');
93
+ return response.redirect(accountPath('orgs'));
95
94
  const org = await store.findOrgById(orgId);
96
95
  if (!org)
97
- return response.redirect('/account/orgs');
96
+ return response.redirect(accountPath('orgs'));
98
97
  // Grava cookie de org ativa
99
- const cookieValue = encodeActiveOrgCookie({ orgId, orgSlug: org.slug, orgRole: membership.role });
98
+ const cookieValue = encodeActiveOrgCookie({
99
+ orgId,
100
+ orgSlug: org.slug,
101
+ orgRole: membership.role,
102
+ });
100
103
  ctx.response.cookie(ACTIVE_ORG_COOKIE, cookieValue, {
101
104
  httpOnly: true,
102
105
  sameSite: 'lax',
@@ -104,8 +107,12 @@ export default class AccountOrgsController {
104
107
  secure: ctx.request.secure(),
105
108
  path: '/',
106
109
  });
107
- await cfg.audit?.record({ type: 'organization.switched', accountId, metadata: { orgId, orgSlug: org.slug } });
108
- return response.redirect('/account/orgs');
110
+ await cfg.audit?.record({
111
+ type: 'organization.switched',
112
+ accountId,
113
+ metadata: { orgId, orgSlug: org.slug },
114
+ });
115
+ return response.redirect(accountPath('orgs'));
109
116
  }
110
117
  /** POST /account/orgs/deactivate — remove org ativa. */
111
118
  async deactivate(ctx) {
@@ -115,7 +122,7 @@ export default class AccountOrgsController {
115
122
  const accountId = session.get(ACCOUNT_SESSION_KEY);
116
123
  ctx.response.clearCookie(ACTIVE_ORG_COOKIE, { path: '/' });
117
124
  await cfg.audit?.record({ type: 'organization.deactivated', accountId });
118
- return response.redirect('/account/orgs');
125
+ return response.redirect(accountPath('orgs'));
119
126
  }
120
127
  /** POST /account/orgs/:id/leave — sai da org (verifica last_owner). */
121
128
  async leave(ctx) {
@@ -128,9 +135,13 @@ export default class AccountOrgsController {
128
135
  return response.forbidden();
129
136
  const result = await store.removeOrgMember(params.id, accountId);
130
137
  if (result.ok) {
131
- await cfg.audit?.record({ type: 'organization.member_removed', accountId, metadata: { orgId: params.id, self: true } });
138
+ await cfg.audit?.record({
139
+ type: 'organization.member_removed',
140
+ accountId,
141
+ metadata: { orgId: params.id, self: true },
142
+ });
132
143
  }
133
- return response.redirect('/account/orgs');
144
+ return response.redirect(accountPath('orgs'));
134
145
  }
135
146
  /** POST /account/orgs/:id/invite — convida membro por e-mail. */
136
147
  async invite(ctx) {
@@ -149,7 +160,7 @@ export default class AccountOrgsController {
149
160
  const email = request.input('email', '').trim();
150
161
  const role = request.input('role', 'member').trim();
151
162
  if (!email)
152
- return response.redirect('/account/orgs');
163
+ return response.redirect(accountPath('orgs'));
153
164
  // H4: valida o role contra o catálogo efetivo de roles da org (runtime →
154
165
  // config → defaults). Role fora do catálogo é rejeitada (não cria convite).
155
166
  // Usa o helper PURO `isRoleInCatalog` (mesmo ponto de verdade do caminho admin).
@@ -160,7 +171,9 @@ export default class AccountOrgsController {
160
171
  invitationTtlHours: cfg.organizations.invitationTtlHours,
161
172
  }, params.id);
162
173
  if (!roleValid) {
163
- return response.unprocessableEntity({ error: { code: 'invalid_role', message: 'Role inválida.' } });
174
+ return response.unprocessableEntity({
175
+ error: { code: 'invalid_role', message: 'Role inválida.' },
176
+ });
164
177
  }
165
178
  // H4: no fluxo member-facing, só um OWNER pode conceder a role `owner`.
166
179
  // Um admin (não-owner) tentando convidar como `owner` é escalonamento.
@@ -177,7 +190,7 @@ export default class AccountOrgsController {
177
190
  // Dispara e-mail via mail hook (best-effort)
178
191
  if (cfg.mail?.onOrgInvitation) {
179
192
  const org = await store.findOrgById(params.id);
180
- const acceptUrl = `${ctx.request.protocol()}://${ctx.request.host()}/account/orgs/invitations/${token}/accept`;
193
+ const acceptUrl = `${ctx.request.protocol()}://${ctx.request.host()}${accountPath('orgs')}/invitations/${token}/accept`;
181
194
  try {
182
195
  await cfg.mail.onOrgInvitation({
183
196
  email,
@@ -198,7 +211,7 @@ export default class AccountOrgsController {
198
211
  accountId,
199
212
  metadata: { orgId: params.id, email, role },
200
213
  });
201
- return response.redirect('/account/orgs');
214
+ return response.redirect(accountPath('orgs'));
202
215
  }
203
216
  /** GET /account/orgs/invitations/:token/accept — mostra tela de aceite. */
204
217
  async showAcceptInvitation(ctx) {
@@ -213,7 +226,7 @@ export default class AccountOrgsController {
213
226
  if (!accountId) {
214
227
  // Não logado: redireciona para login (configurável) com return URL
215
228
  const loginUrl = getAccountLoginUrl();
216
- const returnTo = encodeURIComponent(`/account/orgs/invitations/${params.token}/accept`);
229
+ const returnTo = encodeURIComponent(`${accountPath('orgs')}/invitations/${params.token}/accept`);
217
230
  const sep = loginUrl.includes('?') ? '&' : '?';
218
231
  return response.redirect(`${loginUrl}${sep}returnTo=${returnTo}`);
219
232
  }
@@ -243,7 +256,7 @@ export default class AccountOrgsController {
243
256
  const tokenHash = createHash('sha256').update(params.token).digest('hex');
244
257
  const invitation = await store.findInvitationByTokenHash(tokenHash);
245
258
  if (!invitation)
246
- return response.redirect('/account/orgs');
259
+ return response.redirect(accountPath('orgs'));
247
260
  const result = await store.acceptInvitation(invitation.id, accountId);
248
261
  if (result.ok) {
249
262
  await cfg.audit?.record({
@@ -252,7 +265,7 @@ export default class AccountOrgsController {
252
265
  metadata: { orgId: invitation.organizationId, invitationId: invitation.id },
253
266
  });
254
267
  }
255
- return response.redirect('/account/orgs');
268
+ return response.redirect(accountPath('orgs'));
256
269
  }
257
270
  /** POST /account/orgs/:id/members/:accountId/remove */
258
271
  async removeMember(ctx) {
@@ -264,7 +277,8 @@ export default class AccountOrgsController {
264
277
  if (!supportsOrganizations(store))
265
278
  return response.forbidden();
266
279
  const actorMembership = await store.getOrgMembership(params.id, actorId);
267
- if (!actorMembership || (actorMembership.role !== 'owner' && actorMembership.role !== 'admin')) {
280
+ if (!actorMembership ||
281
+ (actorMembership.role !== 'owner' && actorMembership.role !== 'admin')) {
268
282
  return response.forbidden();
269
283
  }
270
284
  const result = await store.removeOrgMember(params.id, params.accountId);
@@ -275,7 +289,7 @@ export default class AccountOrgsController {
275
289
  metadata: { orgId: params.id, targetAccountId: params.accountId },
276
290
  });
277
291
  }
278
- return response.redirect('/account/orgs');
292
+ return response.redirect(accountPath('orgs'));
279
293
  }
280
294
  /** POST /account/orgs/:id/invitations/:invId/revoke */
281
295
  async revokeInvitation(ctx) {
@@ -287,7 +301,8 @@ export default class AccountOrgsController {
287
301
  if (!supportsOrganizations(store))
288
302
  return response.forbidden();
289
303
  const actorMembership = await store.getOrgMembership(params.id, actorId);
290
- if (!actorMembership || (actorMembership.role !== 'owner' && actorMembership.role !== 'admin')) {
304
+ if (!actorMembership ||
305
+ (actorMembership.role !== 'owner' && actorMembership.role !== 'admin')) {
291
306
  return response.forbidden();
292
307
  }
293
308
  // Escopado por org (params.id já validado acima): impede IDOR cross-org.
@@ -297,7 +312,7 @@ export default class AccountOrgsController {
297
312
  actorId,
298
313
  metadata: { orgId: params.id, invitationId: params.invId },
299
314
  });
300
- return response.redirect('/account/orgs');
315
+ return response.redirect(accountPath('orgs'));
301
316
  }
302
317
  // ──────────────────────────────────────────────────────────────────────────
303
318
  // JSON endpoints — consumed by @adonis-agora/authkit-react hooks.
@@ -356,7 +371,12 @@ export default class AccountOrgsController {
356
371
  const members = canManage ? await store.listOrgMembers(params.id) : [];
357
372
  const enrichedMembers = await Promise.all(members.map(async (m) => {
358
373
  const account = await store.findById(m.accountId);
359
- return { accountId: m.accountId, email: account?.email ?? null, role: m.role, joinedAt: m.joinedAt };
374
+ return {
375
+ accountId: m.accountId,
376
+ email: account?.email ?? null,
377
+ role: m.role,
378
+ joinedAt: m.joinedAt,
379
+ };
360
380
  }));
361
381
  return {
362
382
  id: org.id,
@@ -1,5 +1,5 @@
1
- import "../augmentations.js";
2
- import type { HttpContext } from "@adonisjs/core/http";
1
+ import '../augmentations.js';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
3
  /**
4
4
  * Self-service de segurança da conta (console de conta): trocar a senha e o
5
5
  * e-mail. A troca de senha exige a senha ATUAL (verifyCredentials). A troca de