@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.
- package/build/index.d.ts +2 -0
- package/build/index.js +1 -0
- package/build/src/accounts/account_store.d.ts +14 -3
- package/build/src/accounts/lucid_store/core.js +5 -4
- package/build/src/audit/audit_sink.d.ts +14 -3
- package/build/src/audit/lucid_audit_sink.js +5 -4
- package/build/src/commands/expire_scan_command.js +9 -8
- package/build/src/host/account_export_service.js +4 -3
- package/build/src/host/admin_api/admin_orgs_service.js +6 -5
- package/build/src/host/admin_api/admin_users_service.d.ts +3 -3
- package/build/src/host/admin_api/admin_users_service.js +8 -7
- package/build/src/host/admin_api/api_misc_controller.d.ts +6 -4
- package/build/src/host/admin_api/api_misc_controller.js +6 -5
- package/build/src/host/admin_api/api_users_controller.d.ts +6 -4
- package/build/src/host/admin_api/api_users_controller.js +6 -6
- package/build/src/host/admin_console/console_audit_controller.d.ts +6 -4
- package/build/src/host/admin_console/console_audit_controller.js +6 -5
- package/build/src/host/admin_console/console_overview_controller.js +4 -1
- package/build/src/host/admin_console/console_users_controller.d.ts +6 -4
- package/build/src/host/admin_console/console_users_controller.js +6 -6
- package/build/src/host/admin_stats_service.js +7 -5
- package/build/src/host/assets/webauthn.js +2 -2
- package/build/src/host/login_attempt.js +3 -2
- package/build/src/host/login_notify.js +3 -2
- package/build/src/host/session_context.js +3 -2
- package/build/src/host/ui-dist/assets/{index-Dp__4wlB.js → index-CkCQ94la.js} +3 -3
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/pagination.d.ts +77 -0
- package/build/src/pagination.js +66 -0
- 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
|
-
/**
|
|
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
|
-
/**
|
|
32
|
-
|
|
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 =
|
|
365
|
-
const
|
|
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) *
|
|
381
|
-
.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
|
-
/**
|
|
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
|
-
/**
|
|
50
|
-
|
|
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 =
|
|
39
|
-
const
|
|
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) *
|
|
53
|
-
.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 =
|
|
80
|
-
const
|
|
80
|
+
let page = LIST_FIRST_PAGE;
|
|
81
|
+
const size = ADMIN_LIST_DEFAULT_SIZE;
|
|
81
82
|
while (true) {
|
|
82
|
-
const batch = await accountStore.listAccounts({ page,
|
|
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 <
|
|
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:
|
|
161
|
-
|
|
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:
|
|
185
|
-
|
|
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
|
|
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:
|
|
106
|
-
|
|
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
|
|
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 =
|
|
58
|
-
const
|
|
58
|
+
let page = LIST_FIRST_PAGE;
|
|
59
|
+
const size = ADMIN_LIST_DEFAULT_SIZE;
|
|
59
60
|
while (true) {
|
|
60
|
-
const result = await store.listAccounts({ page,
|
|
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 <
|
|
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 <
|
|
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 *
|
|
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 `
|
|
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 <
|
|
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 *
|
|
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 `
|
|
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
|
|
192
|
-
let page =
|
|
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
|
-
|
|
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 <
|
|
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 —
|
|
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 —
|
|
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 =
|
|
23
|
-
const
|
|
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,
|
|
27
|
-
return {
|
|
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 —
|
|
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 —
|
|
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 =
|
|
35
|
-
const
|
|
36
|
-
const result = await cfg.accountStore.listAccounts({ search, page,
|
|
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 {
|
|
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=&
|
|
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=&
|
|
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 =
|
|
19
|
-
const
|
|
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,
|
|
23
|
-
return {
|
|
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'
|
|
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=&
|
|
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=&
|
|
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 =
|
|
57
|
-
const
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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
|
|
69
|
-
let page =
|
|
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,
|
|
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 <
|
|
91
|
+
if (result.data.length < size)
|
|
90
92
|
break;
|
|
91
93
|
page += 1;
|
|
92
94
|
}
|