@adonis-agora/authkit-server 0.65.3 → 0.66.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 (30) hide show
  1. package/build/index.d.ts +2 -0
  2. package/build/index.js +1 -0
  3. package/build/src/accounts/account_store.d.ts +14 -3
  4. package/build/src/accounts/lucid_store/core.js +5 -4
  5. package/build/src/audit/audit_sink.d.ts +14 -3
  6. package/build/src/audit/lucid_audit_sink.js +5 -4
  7. package/build/src/commands/expire_scan_command.js +9 -8
  8. package/build/src/host/account_export_service.js +4 -3
  9. package/build/src/host/admin_api/admin_orgs_service.js +6 -5
  10. package/build/src/host/admin_api/admin_users_service.d.ts +3 -3
  11. package/build/src/host/admin_api/admin_users_service.js +8 -7
  12. package/build/src/host/admin_api/api_misc_controller.d.ts +6 -4
  13. package/build/src/host/admin_api/api_misc_controller.js +6 -5
  14. package/build/src/host/admin_api/api_users_controller.d.ts +6 -4
  15. package/build/src/host/admin_api/api_users_controller.js +6 -6
  16. package/build/src/host/admin_console/console_audit_controller.d.ts +6 -4
  17. package/build/src/host/admin_console/console_audit_controller.js +6 -5
  18. package/build/src/host/admin_console/console_overview_controller.js +4 -1
  19. package/build/src/host/admin_console/console_users_controller.d.ts +6 -4
  20. package/build/src/host/admin_console/console_users_controller.js +6 -6
  21. package/build/src/host/admin_stats_service.js +7 -5
  22. package/build/src/host/assets/webauthn.js +2 -2
  23. package/build/src/host/login_attempt.js +3 -2
  24. package/build/src/host/login_notify.js +3 -2
  25. package/build/src/host/session_context.js +3 -2
  26. package/build/src/host/ui-dist/assets/{index-Dp__4wlB.js → index-CkCQ94la.js} +3 -3
  27. package/build/src/host/ui-dist/index.html +1 -1
  28. package/build/src/pagination.d.ts +77 -0
  29. package/build/src/pagination.js +66 -0
  30. package/package.json +12 -12
package/build/index.d.ts CHANGED
@@ -109,6 +109,8 @@ export type { ProviderIdentityClass, ProviderIdentityRow, } from './src/mixins/w
109
109
  export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
110
110
  export type { WebauthnCredentialClass, WebauthnCredentialRow, } from './src/mixins/with_webauthn_credential.js';
111
111
  export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
112
+ export type { ListMeta, PaginatedResponse } from './src/pagination.js';
113
+ export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize, parseListPage, parseListSize, resolveListPage, } from './src/pagination.js';
112
114
  export type { LegacyPasswordVerifier, PasswordConfigInput, PasswordVerifyResult, } from './src/password/password_manager.js';
113
115
  export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
114
116
  export type { PasswordPolicyViolation, ResolvedPasswordConfig, ResolvedPasswordPolicy, } from './src/password/policy.js';
package/build/index.js CHANGED
@@ -101,6 +101,7 @@ export { withMfa } from './src/mixins/with_mfa.js';
101
101
  export { withPersonalAccessToken } from './src/mixins/with_personal_access_token.js';
102
102
  export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
103
103
  export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
104
+ export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize, parseListPage, parseListSize, resolveListPage, } from './src/pagination.js';
104
105
  // Gerência de senha: lazy rehash + legacy verifier, política e checagem de vazamento.
105
106
  export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
106
107
  export { checkPasswordPolicy, DEFAULT_PWNED_TIMEOUT_MS, policyViolationParams, } from './src/password/policy.js';
@@ -22,14 +22,25 @@ export interface LinkProviderIdentityInput {
22
22
  providerUserId: string;
23
23
  email?: string;
24
24
  }
25
- /** Parâmetros de listagem paginada de contas (console admin). */
25
+ /**
26
+ * Parâmetros de listagem paginada de contas (console admin).
27
+ *
28
+ * O par `{ page, size }` espelha INTENCIONALMENTE o shape de paginação por
29
+ * offset de `@adonis-agora/filter` (`FilterInput.page`/`.size`), para manter a
30
+ * mesma interface em todo o ecossistema `@adonis-agora/*`. É um casamento
31
+ * ESTRUTURAL: não há dependência de `@adonis-agora/filter` aqui.
32
+ */
26
33
  export interface ListAccountsParams {
27
34
  /** Filtro por e-mail (substring, case-insensitive). */
28
35
  search?: string;
29
36
  /** Página (1-based). Default: 1. */
30
37
  page?: number;
31
- /** Itens por página. Default: 20. */
32
- limit?: number;
38
+ /**
39
+ * Itens por página. Default: 20; limitado a `ADMIN_LIST_MAX_SIZE` (200).
40
+ *
41
+ * Renomeado de `limit` para `size` para casar com `@adonis-agora/filter`.
42
+ */
43
+ size?: number;
33
44
  }
34
45
  /** Página de resultados + total absoluto (para paginação na UI). */
35
46
  export interface Paginated<T> {
@@ -2,6 +2,7 @@ import { randomBytes } from 'node:crypto';
2
2
  import { Scrypt } from '@adonisjs/core/hash/drivers/scrypt';
3
3
  import { DateTime } from 'luxon';
4
4
  import { decodeOtpToken, encodeOtpToken, evaluateLoginOtp, generateOtpCode, hashLoginOtp, linkTokenFromOtpUrl, OTP_LOGIN_PREFIX, } from '../../host/otp_login.js';
5
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, normalizeListSize, resolveListPage, } from '../../pagination.js';
5
6
  import { hasColumn } from './status_profile.js';
6
7
  import { generateExpiringHashedToken, generateHashedToken, parseExpiringTokenExp, rawToDbToken, rawToExpiringDbToken, sha256Hex, } from './token_hash.js';
7
8
  /**
@@ -361,8 +362,8 @@ export function buildCore(ctx) {
361
362
  },
362
363
  // ----- Administração (console admin) -----
363
364
  async listAccounts(params) {
364
- const page = Math.max(1, params.page ?? 1);
365
- const limit = Math.max(1, params.limit ?? 20);
365
+ const page = resolveListPage(params.page);
366
+ const size = normalizeListSize(params.size, ADMIN_LIST_HTTP_DEFAULT_SIZE);
366
367
  const search = params.search?.trim();
367
368
  const base = () => {
368
369
  const q = Model.query();
@@ -377,8 +378,8 @@ export function buildCore(ctx) {
377
378
  const total = Number(countResult[0]?.$extras?.total ?? 0);
378
379
  const rows = await base()
379
380
  .orderBy('email', 'asc')
380
- .offset((page - 1) * limit)
381
- .limit(limit);
381
+ .offset((page - 1) * size)
382
+ .limit(size);
382
383
  return { data: rows.map(toAccount), total };
383
384
  },
384
385
  async setGlobalRoles(accountId, roles) {
@@ -42,12 +42,23 @@ export interface StoredAuditEvent extends AuditEvent {
42
42
  id: string;
43
43
  createdAt: Date | string | null;
44
44
  }
45
- /** Filtros de listagem do log de auditoria (console admin). */
45
+ /**
46
+ * Filtros de listagem do log de auditoria (console admin).
47
+ *
48
+ * O par `{ page, size }` espelha INTENCIONALMENTE o shape de paginação por
49
+ * offset de `@adonis-agora/filter` (`FilterInput.page`/`.size`), para manter a
50
+ * mesma interface em todo o ecossistema `@adonis-agora/*`. É um casamento
51
+ * ESTRUTURAL: não há dependência de `@adonis-agora/filter` aqui.
52
+ */
46
53
  export interface ListAuditParams {
47
54
  /** Página (1-based). Default: 1. */
48
55
  page?: number;
49
- /** Itens por página. Default: 20. */
50
- limit?: number;
56
+ /**
57
+ * Itens por página. Default: 20; limitado a `ADMIN_LIST_MAX_SIZE` (200).
58
+ *
59
+ * Renomeado de `limit` para `size` para casar com `@adonis-agora/filter`.
60
+ */
61
+ size?: number;
51
62
  /** Filtra por tipo de evento exato. */
52
63
  type?: string;
53
64
  /** Filtra pelo subject (accountId) do evento. */
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, normalizeListSize, resolveListPage } from '../pagination.js';
2
3
  /**
3
4
  * Pseudônimo estável para um accountId anonimizado: `anon:<sha256(accountId)[:16]>`.
4
5
  * Mantém os eventos de uma conta deletada correlacionáveis ENTRE SI (mesmo
@@ -35,8 +36,8 @@ export function lucidAuditSink(Model) {
35
36
  }
36
37
  },
37
38
  async list(params) {
38
- const page = Math.max(1, params.page ?? 1);
39
- const limit = Math.max(1, params.limit ?? 20);
39
+ const page = resolveListPage(params.page);
40
+ const size = normalizeListSize(params.size, ADMIN_LIST_HTTP_DEFAULT_SIZE);
40
41
  const base = () => {
41
42
  const q = Model.query();
42
43
  if (params.type)
@@ -49,8 +50,8 @@ export function lucidAuditSink(Model) {
49
50
  const total = Number(countResult[0]?.$extras?.total ?? 0);
50
51
  const rows = await base()
51
52
  .orderBy('createdAt', 'desc')
52
- .offset((page - 1) * limit)
53
- .limit(limit);
53
+ .offset((page - 1) * size)
54
+ .limit(size);
54
55
  const data = rows.map((row) => ({
55
56
  id: String(row.id),
56
57
  type: row.type,
@@ -29,6 +29,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
29
29
  };
30
30
  import { RuntimeSettings } from '../host/runtime_settings.js';
31
31
  import { resolveEffectiveAccountExpiration } from '../host/runtime_toggles.js';
32
+ import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
32
33
  import { resolveAuthkitConfig } from './resolve_config.js';
33
34
  // ---------------------------------------------------------------------------
34
35
  // Core logic (pura, testável sem Ace)
@@ -76,10 +77,10 @@ export async function runExpireScan(app, opts) {
76
77
  let warnedCount = 0;
77
78
  let dedupedCount = 0;
78
79
  let scanned = 0;
79
- let page = 1;
80
- const limit = 100;
80
+ let page = LIST_FIRST_PAGE;
81
+ const size = ADMIN_LIST_DEFAULT_SIZE;
81
82
  while (true) {
82
- const batch = await accountStore.listAccounts({ page, limit });
83
+ const batch = await accountStore.listAccounts({ page, size });
83
84
  if (batch.data.length === 0)
84
85
  break;
85
86
  for (const account of batch.data) {
@@ -127,7 +128,7 @@ export async function runExpireScan(app, opts) {
127
128
  }
128
129
  }
129
130
  }
130
- if (batch.data.length < limit)
131
+ if (batch.data.length < size)
131
132
  break;
132
133
  page++;
133
134
  }
@@ -157,8 +158,8 @@ async function getLastLoginMs(audit, accountId) {
157
158
  const result = await audit.list({
158
159
  type: 'login.success',
159
160
  subject: accountId,
160
- page: 1,
161
- limit: 1,
161
+ page: LIST_FIRST_PAGE,
162
+ size: 1,
162
163
  });
163
164
  if (result.data.length === 0)
164
165
  return null;
@@ -181,8 +182,8 @@ async function wasWarnedRecently(audit, accountId, warnDays) {
181
182
  const result = await audit.list({
182
183
  type: 'account.expiration_warned',
183
184
  subject: accountId,
184
- page: 1,
185
- limit: 1,
185
+ page: LIST_FIRST_PAGE,
186
+ size: 1,
186
187
  });
187
188
  if (result.data.length === 0)
188
189
  return false;
@@ -1,7 +1,8 @@
1
1
  import { supportsOrganizations, supportsPasskeys, supportsProviderIdentity, } from '../accounts/account_store.js';
2
+ import { LIST_FIRST_PAGE } from '../pagination.js';
2
3
  import { AdminSessionsService } from './admin_sessions_service.js';
3
4
  /** Limite de eventos de audit incluídos no export (evita payloads gigantes). */
4
- const AUDIT_EXPORT_LIMIT = 1000;
5
+ const AUDIT_EXPORT_SIZE = 1000;
5
6
  /**
6
7
  * Monta o pacote de dados de uma conta para portabilidade (LGPD/GDPR). NUNCA
7
8
  * inclui segredos: hash de senha, segredo TOTP, recovery codes, chave pública de
@@ -102,8 +103,8 @@ export class AccountExportService {
102
103
  try {
103
104
  const page = await cfg.audit.list({
104
105
  subject: accountId,
105
- page: 1,
106
- limit: AUDIT_EXPORT_LIMIT,
106
+ page: LIST_FIRST_PAGE,
107
+ size: AUDIT_EXPORT_SIZE,
107
108
  });
108
109
  auditLog = page.data.map((e) => ({
109
110
  type: e.type,
@@ -1,4 +1,5 @@
1
1
  import { supportsOrganizations } from '../../accounts/account_store.js';
2
+ import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../../pagination.js';
2
3
  import { accountPath } from '../account_paths.js';
3
4
  import { sendOrgInvitationEmail } from '../default_mailer.js';
4
5
  import { isRoleInCatalog, resolveRoleCatalogList } from '../runtime_toggles.js';
@@ -50,14 +51,14 @@ export class AdminOrgsService {
50
51
  // O store não tem listAllOrgs — acumulamos via memberships de todas as contas.
51
52
  // Para admin, fazemos um listAccounts full e coletamos todas as orgs únicas.
52
53
  // Mais eficiente: usa a tabela de organizations diretamente pelo listOrgMembers
53
- // mas o store não expõe listAllOrgs. Usamos hack: listAccounts com limit alto,
54
+ // mas o store não expõe listAllOrgs. Usamos hack: listAccounts com size alto,
54
55
  // coletamos orgs via listOrgsForAccount por conta.
55
56
  // Para evitar N+1 excessivo, usamos uma abordagem "seen" de IDs.
56
57
  const seen = new Map();
57
- let page = 1;
58
- const limit = 100;
58
+ let page = LIST_FIRST_PAGE;
59
+ const size = ADMIN_LIST_DEFAULT_SIZE;
59
60
  while (true) {
60
- const result = await store.listAccounts({ page, limit });
61
+ const result = await store.listAccounts({ page, size });
61
62
  for (const account of result.data) {
62
63
  const orgs = await store.listOrgsForAccount(account.id);
63
64
  for (const org of orgs) {
@@ -68,7 +69,7 @@ export class AdminOrgsService {
68
69
  }
69
70
  }
70
71
  }
71
- if (result.data.length < limit)
72
+ if (result.data.length < size)
72
73
  break;
73
74
  page++;
74
75
  }
@@ -121,12 +121,12 @@ export declare class AdminUsersService {
121
121
  * conta é contada no máximo uma vez via {@link hasAdminRole}).
122
122
  *
123
123
  * Terminadores honestos (sem número mágico): a paginação para quando
124
- * - a página veio incompleta (`data.length < pageSize`) → última página, OU
124
+ * - a página veio incompleta (`data.length < size`) → última página, OU
125
125
  * - a página veio vazia (`data.length === 0`) → sem mais dados, OU
126
- * - já cobrimos o total reportado (`page * pageSize >= total`).
126
+ * - já cobrimos o total reportado (`page * size >= total`).
127
127
  * Uma página COMPLETA com `total` ainda maior continua paginando. Isto encerra
128
128
  * de forma garantida para qualquer store que devolva páginas finitas (a última
129
- * página é, por definição, menor que `pageSize` ou vazia).
129
+ * página é, por definição, menor que `size` ou vazia).
130
130
  */
131
131
  private countAdminsByScan;
132
132
  /**
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { supportsAccountDeletion, supportsAccountStatus, supportsCountByGlobalRole, supportsProfile, } from '../../accounts/account_store.js';
3
+ import { ADMIN_LIST_DEFAULT_SIZE, LIST_FIRST_PAGE } from '../../pagination.js';
3
4
  import { PasswordPolicyError } from '../../password/password_manager.js';
4
5
  import { AccountDeletionService } from '../account_deletion_service.js';
5
6
  import { sendPasswordResetEmail } from '../default_mailer.js';
@@ -179,28 +180,28 @@ export class AdminUsersService {
179
180
  * conta é contada no máximo uma vez via {@link hasAdminRole}).
180
181
  *
181
182
  * Terminadores honestos (sem número mágico): a paginação para quando
182
- * - a página veio incompleta (`data.length < pageSize`) → última página, OU
183
+ * - a página veio incompleta (`data.length < size`) → última página, OU
183
184
  * - a página veio vazia (`data.length === 0`) → sem mais dados, OU
184
- * - já cobrimos o total reportado (`page * pageSize >= total`).
185
+ * - já cobrimos o total reportado (`page * size >= total`).
185
186
  * Uma página COMPLETA com `total` ainda maior continua paginando. Isto encerra
186
187
  * de forma garantida para qualquer store que devolva páginas finitas (a última
187
- * página é, por definição, menor que `pageSize` ou vazia).
188
+ * página é, por definição, menor que `size` ou vazia).
188
189
  */
189
190
  async countAdminsByScan() {
190
191
  const store = this.cfg.accountStore;
191
- const pageSize = 100;
192
- let page = 1;
192
+ const size = ADMIN_LIST_DEFAULT_SIZE;
193
+ let page = LIST_FIRST_PAGE;
193
194
  let count = 0;
194
195
  while (true) {
195
196
  const { data, total } = await store.listAccounts({
196
197
  page,
197
- limit: pageSize,
198
+ size,
198
199
  });
199
200
  for (const acc of data) {
200
201
  if (this.hasAdminRole(acc.globalRoles ?? []))
201
202
  count++;
202
203
  }
203
- if (data.length < pageSize || page * pageSize >= total)
204
+ if (data.length < size || page * size >= total)
204
205
  break;
205
206
  page++;
206
207
  }
@@ -5,12 +5,14 @@ import type { HttpContext } from '@adonisjs/core/http';
5
5
  * introspecção genérica de token (`POST /tokens/verify`).
6
6
  */
7
7
  export default class ApiMiscController {
8
- /** GET /audit — listagem paginada (501 JSON quando o sink não consulta). */
8
+ /** GET /audit — `{ meta: { page, size, total }, data }` (501 JSON quando o sink não consulta). */
9
9
  audit(ctx: HttpContext): Promise<void | {
10
+ meta: {
11
+ page: number;
12
+ size: number;
13
+ total: any;
14
+ };
10
15
  data: any;
11
- total: any;
12
- page: number;
13
- limit: number;
14
16
  }>;
15
17
  /** GET /stats — métricas-resumo do IdP (totais + MAU + séries de 30 dias). */
16
18
  stats(ctx: HttpContext): Promise<import("../admin_stats_service.js").AdminStats>;
@@ -1,4 +1,5 @@
1
1
  import '../augmentations.js';
2
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
2
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
3
4
  import { computeAdminStats } from '../admin_stats_service.js';
4
5
  import { tokenVerifyValidator } from '../admin_validators.js';
@@ -9,7 +10,7 @@ import { TokenVerifyService } from './token_verify_service.js';
9
10
  * introspecção genérica de token (`POST /tokens/verify`).
10
11
  */
11
12
  export default class ApiMiscController {
12
- /** GET /audit — listagem paginada (501 JSON quando o sink não consulta). */
13
+ /** GET /audit — `{ meta: { page, size, total }, data }` (501 JSON quando o sink não consulta). */
13
14
  async audit(ctx) {
14
15
  const service = await ctx.containerResolver.make('authkit.server');
15
16
  const cfg = service.config;
@@ -19,12 +20,12 @@ export default class ApiMiscController {
19
20
  .status(501)
20
21
  .send(apiError('not_implemented', 'O sink de auditoria configurado não suporta consulta.'));
21
22
  }
22
- const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
23
- const limit = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('limit', '20'), 10) || 20));
23
+ const page = parseListPage(ctx.request.input('page'));
24
+ const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
24
25
  const type = ctx.request.input('type')?.trim() || undefined;
25
26
  const subject = ctx.request.input('subject')?.trim() || undefined;
26
- const result = await sink.list({ page, limit, type, subject });
27
- return { data: result.data.map(auditDto), total: result.total, page, limit };
27
+ const result = await sink.list({ page, size, type, subject });
28
+ return { meta: { page, size, total: result.total }, data: result.data.map(auditDto) };
28
29
  }
29
30
  /** GET /stats — métricas-resumo do IdP (totais + MAU + séries de 30 dias). */
30
31
  async stats(ctx) {
@@ -6,12 +6,14 @@ import type { HttpContext } from '@adonisjs/core/http';
6
6
  */
7
7
  export default class ApiUsersController {
8
8
  #private;
9
- /** GET /users — listagem paginada com busca por e-mail. */
9
+ /** GET /users — `{ meta: { page, size, total }, data }`, com busca por e-mail. */
10
10
  index(ctx: HttpContext): Promise<{
11
+ meta: {
12
+ page: number;
13
+ size: number;
14
+ total: any;
15
+ };
11
16
  data: any[];
12
- total: any;
13
- page: number;
14
- limit: number;
15
17
  }>;
16
18
  /** GET /users/:id */
17
19
  show(ctx: HttpContext): Promise<void | {
@@ -1,11 +1,11 @@
1
1
  import '../augmentations.js';
2
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
2
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
3
4
  import { adminUserCreateValidator, adminUserUpdateValidator } from '../admin_validators.js';
4
5
  import { resolveRuntimeSettings } from '../runtime_settings.js';
5
6
  import { enrichSessionsWithContext } from '../session_context.js';
6
7
  import { AdminUsersService } from './admin_users_service.js';
7
8
  import { apiError, grantDto, sessionDto, userDto } from './dto.js';
8
- const PAGE_SIZE = 20;
9
9
  /**
10
10
  * Lê a config + monta o actor `admin-api` para auditoria. O `actorId` recebe o
11
11
  * id NÃO-SENSÍVEL da API key que autenticou (anexado pelo `adminApiGuard` em
@@ -27,16 +27,16 @@ async function ctxBits(ctx) {
27
27
  * envelope `{ error: { code, message } }`. Toda escrita audita com `actor: 'admin-api'`.
28
28
  */
29
29
  export default class ApiUsersController {
30
- /** GET /users — listagem paginada com busca por e-mail. */
30
+ /** GET /users — `{ meta: { page, size, total }, data }`, com busca por e-mail. */
31
31
  async index(ctx) {
32
32
  const { cfg } = await ctxBits(ctx);
33
33
  const search = ctx.request.input('search', '').trim();
34
- const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
35
- const limit = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('limit', String(PAGE_SIZE)), 10) || PAGE_SIZE));
36
- const result = await cfg.accountStore.listAccounts({ search, page, limit });
34
+ const page = parseListPage(ctx.request.input('page'));
35
+ const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
36
+ const result = await cfg.accountStore.listAccounts({ search, page, size });
37
37
  const users = new AdminUsersService(cfg);
38
38
  const data = await Promise.all(result.data.map(async (u) => userDto(u, await users.isDisabled(u.id))));
39
- return { data, total: result.total, page, limit };
39
+ return { meta: { page, size, total: result.total }, data };
40
40
  }
41
41
  /** GET /users/:id */
42
42
  async show(ctx) {
@@ -3,15 +3,17 @@ import type { HttpContext } from '@adonisjs/core/http';
3
3
  /**
4
4
  * Endpoints JSON do log de auditoria do console admin React.
5
5
  *
6
- * GET {prefix}/api/audit?type=&page=&limit= → listagem paginada
6
+ * GET {prefix}/api/audit?type=&page=&size= → `{ meta: { page, size, total }, data }`
7
7
  *
8
8
  * 404 honesto (`capability_unsupported`) quando o sink não suporta consulta.
9
9
  */
10
10
  export default class ConsoleAuditController {
11
11
  index(ctx: HttpContext): Promise<void | {
12
+ meta: {
13
+ page: number;
14
+ size: number;
15
+ total: any;
16
+ };
12
17
  data: any;
13
- total: any;
14
- page: number;
15
- limit: number;
16
18
  }>;
17
19
  }
@@ -1,9 +1,10 @@
1
1
  import '../augmentations.js';
2
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
2
3
  import { apiError, auditDto } from '../admin_api/dto.js';
3
4
  /**
4
5
  * Endpoints JSON do log de auditoria do console admin React.
5
6
  *
6
- * GET {prefix}/api/audit?type=&page=&limit= → listagem paginada
7
+ * GET {prefix}/api/audit?type=&page=&size= → `{ meta: { page, size, total }, data }`
7
8
  *
8
9
  * 404 honesto (`capability_unsupported`) quando o sink não suporta consulta.
9
10
  */
@@ -15,11 +16,11 @@ export default class ConsoleAuditController {
15
16
  if (!sink || typeof sink.list !== 'function') {
16
17
  return ctx.response.notFound(apiError('capability_unsupported', 'O sink de auditoria configurado não suporta consulta.'));
17
18
  }
18
- const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
19
- const limit = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('limit', '20'), 10) || 20));
19
+ const page = parseListPage(ctx.request.input('page'));
20
+ const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
20
21
  const type = ctx.request.input('type')?.trim() || undefined;
21
22
  const subject = ctx.request.input('subject')?.trim() || undefined;
22
- const result = await sink.list({ page, limit, type, subject });
23
- return { data: result.data.map(auditDto), total: result.total, page, limit };
23
+ const result = await sink.list({ page, size, type, subject });
24
+ return { meta: { page, size, total: result.total }, data: result.data.map(auditDto) };
24
25
  }
25
26
  }
@@ -1,4 +1,5 @@
1
1
  import '../augmentations.js';
2
+ import { LIST_FIRST_PAGE } from '../../pagination.js';
2
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
3
4
  import { computeAdminStats } from '../admin_stats_service.js';
4
5
  /**
@@ -17,7 +18,9 @@ export default class ConsoleOverviewController {
17
18
  const sessions = new AdminSessionsService(service);
18
19
  const stats = await computeAdminStats(cfg, sessions);
19
20
  const clientsCount = cfg.clients.length;
20
- const recentResult = typeof cfg.audit?.list === 'function' ? await cfg.audit.list({ page: 1, limit: 5 }) : null;
21
+ const recentResult = typeof cfg.audit?.list === 'function'
22
+ ? await cfg.audit.list({ page: LIST_FIRST_PAGE, size: 5 })
23
+ : null;
21
24
  return {
22
25
  usersTotal: stats.totalUsers,
23
26
  activeSessions: stats.activeSessions,
@@ -3,7 +3,7 @@ import type { HttpContext } from '@adonisjs/core/http';
3
3
  /**
4
4
  * Endpoints JSON de usuários do console admin React.
5
5
  *
6
- * GET {prefix}/api/users?search=&page=&perPage= → lista paginada + roles
6
+ * GET {prefix}/api/users?search=&page=&size= → `{ meta: { page, size, total }, data }`
7
7
  * GET {prefix}/api/users/:id → detalhe + sessões + identidades + MFA status
8
8
  * POST {prefix}/api/users → criar usuário
9
9
  * PATCH {prefix}/api/users/:id/roles → substituir roles globais
@@ -20,10 +20,12 @@ export default class ConsoleUsersController {
20
20
  #private;
21
21
  /** GET {prefix}/api/users */
22
22
  index(ctx: HttpContext): Promise<{
23
+ meta: {
24
+ page: number;
25
+ size: number;
26
+ total: any;
27
+ };
23
28
  data: any[];
24
- total: any;
25
- page: number;
26
- perPage: number;
27
29
  }>;
28
30
  /** GET {prefix}/api/users/:id */
29
31
  show(ctx: HttpContext): Promise<void | {
@@ -1,5 +1,6 @@
1
1
  import '../augmentations.js';
2
2
  import { supportsAccountDeletion, supportsAccountStatus } from '../../accounts/account_store.js';
3
+ import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
3
4
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
4
5
  import { AdminUsersService } from '../admin_api/admin_users_service.js';
5
6
  import { apiError, grantDto, sessionDto, userDto } from '../admin_api/dto.js';
@@ -9,7 +10,6 @@ import { resolveRuntimeSettings } from '../runtime_settings.js';
9
10
  import { resolveEffectiveRolesCatalog } from '../runtime_toggles.js';
10
11
  import { enrichSessionsWithContext } from '../session_context.js';
11
12
  import { requireSudo } from '../sudo_mode.js';
12
- const PAGE_SIZE = 20;
13
13
  /**
14
14
  * Gate de sudo (M9) para ações destrutivas do console (delete user, disable,
15
15
  * reset-password): exige confirmação de identidade RECENTE do próprio admin
@@ -34,7 +34,7 @@ async function gateSudo(ctx) {
34
34
  /**
35
35
  * Endpoints JSON de usuários do console admin React.
36
36
  *
37
- * GET {prefix}/api/users?search=&page=&perPage= → lista paginada + roles
37
+ * GET {prefix}/api/users?search=&page=&size= → `{ meta: { page, size, total }, data }`
38
38
  * GET {prefix}/api/users/:id → detalhe + sessões + identidades + MFA status
39
39
  * POST {prefix}/api/users → criar usuário
40
40
  * PATCH {prefix}/api/users/:id/roles → substituir roles globais
@@ -53,16 +53,16 @@ export default class ConsoleUsersController {
53
53
  const service = await ctx.containerResolver.make('authkit.server');
54
54
  const cfg = service.config;
55
55
  const search = ctx.request.input('search', '').trim();
56
- const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
57
- const perPage = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('perPage', String(PAGE_SIZE)), 10) || PAGE_SIZE));
56
+ const page = parseListPage(ctx.request.input('page'));
57
+ const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
58
58
  const result = await cfg.accountStore.listAccounts({
59
59
  search,
60
60
  page,
61
- limit: perPage,
61
+ size,
62
62
  });
63
63
  const users = new AdminUsersService(cfg);
64
64
  const data = await Promise.all(result.data.map(async (u) => userDto(u, await users.isDisabled(u.id))));
65
- return { data, total: result.total, page, perPage };
65
+ return { meta: { page, size, total: result.total }, data };
66
66
  }
67
67
  /** GET {prefix}/api/users/:id */
68
68
  async show(ctx) {
@@ -1,3 +1,4 @@
1
+ import { ADMIN_LIST_MAX_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
1
2
  /** Janela de agregação (dias). */
2
3
  const WINDOW_DAYS = 30;
3
4
  /**
@@ -19,7 +20,8 @@ const MAX_EVENTS = 10000;
19
20
  * nativa e expor um `stats()` próprio no futuro.
20
21
  */
21
22
  export async function computeAdminStats(cfg, sessionsService) {
22
- const totalUsers = (await cfg.accountStore.listAccounts({ page: 1, limit: 1 })).total;
23
+ const totalUsers = (await cfg.accountStore.listAccounts({ page: LIST_FIRST_PAGE, size: 1 }))
24
+ .total;
23
25
  const activeSessions = sessionsService.canList
24
26
  ? await sessionsService.countActiveSessions()
25
27
  : null;
@@ -65,10 +67,10 @@ export async function computeAdminStats(cfg, sessionsService) {
65
67
  */
66
68
  async function loadEventsInWindow(audit, type, windowStartMs) {
67
69
  const out = [];
68
- const limit = 200;
69
- let page = 1;
70
+ const size = ADMIN_LIST_MAX_SIZE;
71
+ let page = LIST_FIRST_PAGE;
70
72
  while (out.length < MAX_EVENTS) {
71
- const result = await audit.list({ type, page, limit });
73
+ const result = await audit.list({ type, page, size });
72
74
  if (result.data.length === 0)
73
75
  break;
74
76
  let allBelowWindow = true;
@@ -86,7 +88,7 @@ async function loadEventsInWindow(audit, type, windowStartMs) {
86
88
  // Página inteira anterior à janela → não há mais nada relevante (ordem desc).
87
89
  if (allBelowWindow)
88
90
  break;
89
- if (result.data.length < limit)
91
+ if (result.data.length < size)
90
92
  break;
91
93
  page += 1;
92
94
  }