@adonis-agora/authkit-server 0.65.3 → 0.66.1
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/host/views/account/apps.edge +2 -2
- package/build/host/views/account/confirm.edge +1 -1
- package/build/host/views/account/login.edge +2 -2
- package/build/host/views/account/mfa.edge +5 -5
- package/build/host/views/account/security.edge +17 -17
- package/build/host/views/account/tokens.edge +3 -3
- package/build/host/views/forgot.edge +1 -1
- package/build/host/views/login.edge +2 -2
- package/build/host/views/maintenance.edge +1 -1
- package/build/host/views/mfa-challenge.edge +2 -2
- package/build/host/views/otp-unlock.edge +1 -1
- package/build/host/views/partials/styles.edge +2 -1
- package/build/host/views/reset.edge +1 -1
- package/build/host/views/signup.edge +3 -3
- 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 +14 -14
|
@@ -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
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function l(e){let t=new Uint8Array(e),r="";for(let
|
|
2
|
-
`,t)}function
|
|
1
|
+
function l(e){let t=new Uint8Array(e),r="";for(let i of t)r+=String.fromCharCode(i);return btoa(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function b(e){let t=e.replace(/-/g,"+").replace(/_/g,"/"),r=(4-t.length%4)%4,o=t.padEnd(t.length+r,"="),i=atob(o),u=new ArrayBuffer(i.length),f=new Uint8Array(u);for(let a=0;a<i.length;a++)f[a]=i.charCodeAt(a);return u}function d(){return O.stubThis(globalThis?.PublicKeyCredential!==void 0&&typeof globalThis.PublicKeyCredential=="function")}var O={stubThis:e=>e};function y(e){let{id:t}=e;return{...e,id:b(t),transports:e.transports,type:e.type}}function g(e){return e==="localhost"||/^((xn--[a-z0-9-]+|[a-z0-9]+(-[a-z0-9]+)*)\.)+([a-z]{2,}|xn--[a-z0-9-]+)$/i.test(e)}var n=class extends Error{constructor({message:t,code:r,cause:o,name:i}){super(t,{cause:o}),Object.defineProperty(this,"code",{enumerable:!0,configurable:!0,writable:!0,value:void 0}),this.name=i??o.name,this.code=r}};function P({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new n({message:"Registration ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else if(e.name==="ConstraintError"){if(r.authenticatorSelection?.requireResidentKey===!0)return new n({message:"Discoverable credentials were required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT",cause:e});if(t.mediation==="conditional"&&r.authenticatorSelection?.userVerification==="required")return new n({message:"User verification was required during automatic registration but it could not be performed",code:"ERROR_AUTO_REGISTER_USER_VERIFICATION_FAILURE",cause:e});if(r.authenticatorSelection?.userVerification==="required")return new n({message:"User verification was required but no available authenticator supported it",code:"ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT",cause:e})}else{if(e.name==="InvalidStateError")return new n({message:"The authenticator was previously registered",code:"ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED",cause:e});if(e.name==="NotAllowedError")return new n({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="NotSupportedError")return r.pubKeyCredParams.filter(i=>i.type==="public-key").length===0?new n({message:'No entry in pubKeyCredParams was of type "public-key"',code:"ERROR_MALFORMED_PUBKEYCREDPARAMS",cause:e}):new n({message:"No available authenticator supported any of the specified pubKeyCredParams algorithms",code:"ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG",cause:e});if(e.name==="SecurityError"){let o=globalThis.location.hostname;if(g(o)){if(r.rp.id!==o)return new n({message:`The RP ID "${r.rp.id}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new n({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="TypeError"){if(r.user.id.byteLength<1||r.user.id.byteLength>64)return new n({message:"User ID was not between 1 and 64 characters",code:"ERROR_INVALID_USER_ID_LENGTH",cause:e})}else if(e.name==="UnknownError")return new n({message:"The authenticator was unable to process the specified options, or could not create a new credential",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}var C=class{constructor(){Object.defineProperty(this,"controller",{enumerable:!0,configurable:!0,writable:!0,value:void 0})}createNewAbortSignal(){if(this.controller){let r=new Error("Cancelling existing WebAuthn API call for new one");r.name="AbortError",this.controller.abort(r)}let t=new AbortController;return this.controller=t,t.signal}cancelCeremony(){if(this.controller){let t=new Error("Manually cancelling existing WebAuthn API call");t.name="AbortError",this.controller.abort(t),this.controller=void 0}}},A=new C;var D=["cross-platform","platform"];function R(e){if(e&&!(D.indexOf(e)<0))return e}async function N(e){!e.optionsJSON&&e.challenge&&(console.warn("startRegistration() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useAutoRegister:r=!1}=e;if(!d())throw new Error("WebAuthn is not supported in this browser");let o={...t,challenge:b(t.challenge),user:{...t.user,id:b(t.user.id)},excludeCredentials:t.excludeCredentials?.map(y)},i={};r&&(i.mediation="conditional"),i.publicKey=o,i.signal=A.createNewAbortSignal();let u;try{u=await navigator.credentials.create(i)}catch(c){throw P({error:c,options:i})}if(!u)throw new Error("Registration was not completed");let{id:f,rawId:a,response:s,type:E}=u,p;typeof s.getTransports=="function"&&(p=s.getTransports());let w;if(typeof s.getPublicKeyAlgorithm=="function")try{w=s.getPublicKeyAlgorithm()}catch(c){I("getPublicKeyAlgorithm()",c)}let m;if(typeof s.getPublicKey=="function")try{let c=s.getPublicKey();c!==null&&(m=l(c))}catch(c){I("getPublicKey()",c)}let h;if(typeof s.getAuthenticatorData=="function")try{h=l(s.getAuthenticatorData())}catch(c){I("getAuthenticatorData()",c)}return{id:f,rawId:l(a),response:{attestationObject:l(s.attestationObject),clientDataJSON:l(s.clientDataJSON),transports:p,publicKeyAlgorithm:w,publicKey:m,authenticatorData:h},type:E,clientExtensionResults:u.getClientExtensionResults(),authenticatorAttachment:R(u.authenticatorAttachment)}}function I(e,t){console.warn(`The browser extension that intercepted this WebAuthn API call incorrectly implemented ${e}. You should report this error to them.
|
|
2
|
+
`,t)}function T(){if(!d())return S.stubThis(new Promise(t=>t(!1)));let e=globalThis.PublicKeyCredential;return e?.isConditionalMediationAvailable===void 0?S.stubThis(new Promise(t=>t(!1))):S.stubThis(e.isConditionalMediationAvailable())}var S={stubThis:e=>e};function _({error:e,options:t}){let{publicKey:r}=t;if(!r)throw Error("options was missing required publicKey property");if(e.name==="AbortError"){if(t.signal instanceof AbortSignal)return new n({message:"Authentication ceremony was sent an abort signal",code:"ERROR_CEREMONY_ABORTED",cause:e})}else{if(e.name==="NotAllowedError")return new n({message:e.message,code:"ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY",cause:e});if(e.name==="SecurityError"){let o=globalThis.location.hostname;if(g(o)){if(r.rpId!==o)return new n({message:`The RP ID "${r.rpId}" is invalid for this domain`,code:"ERROR_INVALID_RP_ID",cause:e})}else return new n({message:`${globalThis.location.hostname} is an invalid domain`,code:"ERROR_INVALID_DOMAIN",cause:e})}else if(e.name==="UnknownError")return new n({message:"The authenticator was unable to process the specified options, or could not create a new assertion signature",code:"ERROR_AUTHENTICATOR_GENERAL_ERROR",cause:e})}return e}async function U(e){!e.optionsJSON&&e.challenge&&(console.warn("startAuthentication() was not called correctly. It will try to continue with the provided options, but this call should be refactored to use the expected call structure instead. See https://simplewebauthn.dev/docs/packages/browser#typeerror-cannot-read-properties-of-undefined-reading-challenge for more information."),e={optionsJSON:e});let{optionsJSON:t,useBrowserAutofill:r=!1,verifyBrowserAutofillInput:o=!0}=e;if(!d())throw new Error("WebAuthn is not supported in this browser");let i;t.allowCredentials?.length!==0&&(i=t.allowCredentials?.map(y));let u={...t,challenge:b(t.challenge),allowCredentials:i},f={};if(r){if(!await T())throw Error("Browser does not support WebAuthn autofill");if(document.querySelectorAll("input[autocomplete$='webauthn']").length<1&&o)throw Error('No <input> with "webauthn" as the only or last value in its `autocomplete` attribute was detected');f.mediation="conditional",u.allowCredentials=[]}f.publicKey=u,f.signal=A.createNewAbortSignal();let a;try{a=await navigator.credentials.get(f)}catch(h){throw _({error:h,options:f})}if(!a)throw new Error("Authentication was not completed");let{id:s,rawId:E,response:p,type:w}=a,m;return p.userHandle&&(m=l(p.userHandle)),{id:s,rawId:l(E),response:{authenticatorData:l(p.authenticatorData),clientDataJSON:l(p.clientDataJSON),signature:l(p.signature),userHandle:m},type:w,clientExtensionResults:a.getClientExtensionResults(),authenticatorAttachment:R(a.authenticatorAttachment)}}function x(){return d()?PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable():new Promise(e=>e(!1))}export{A as WebAuthnAbortService,n as WebAuthnError,d as browserSupportsWebAuthn,T as browserSupportsWebAuthnAutofill,x as platformAuthenticatorIsAvailable,U as startAuthentication,N as startRegistration};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { supportsAccountStatus, supportsEmailVerificationStatus, supportsPasswordExpiration, } from '../accounts/account_store.js';
|
|
2
|
+
import { LIST_FIRST_PAGE } from '../pagination.js';
|
|
2
3
|
import { createAccountLockout } from './account_lockout.js';
|
|
3
4
|
import { resolveEffectiveAccountExpiration, resolveEffectiveLockout, resolveEffectivePasswordExpiration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveRequireVerifiedEmailFull, } from './runtime_toggles.js';
|
|
4
5
|
/**
|
|
@@ -100,8 +101,8 @@ export async function isAccountExpired(audit, accountId, settings, logger) {
|
|
|
100
101
|
const result = await audit.list({
|
|
101
102
|
type: 'login.success',
|
|
102
103
|
subject: accountId,
|
|
103
|
-
page:
|
|
104
|
-
|
|
104
|
+
page: LIST_FIRST_PAGE,
|
|
105
|
+
size: 1,
|
|
105
106
|
});
|
|
106
107
|
if (result.data.length === 0) {
|
|
107
108
|
// Nunca logou → considera ativa (conta nova, não inativa).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ADMIN_LIST_MAX_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
|
|
1
2
|
import { sendNewDeviceLoginEmail, sendNewLoginEmail } from './default_mailer.js';
|
|
2
3
|
import { isTrustedDeviceValid, TRUSTED_DEVICE_COOKIE } from './trusted_device.js';
|
|
3
4
|
/**
|
|
@@ -124,8 +125,8 @@ async function maybeNotifyNewLogin(ctx, cfg, data) {
|
|
|
124
125
|
const page = await cfg.audit.list({
|
|
125
126
|
type: 'login.success',
|
|
126
127
|
subject: accountId,
|
|
127
|
-
page:
|
|
128
|
-
|
|
128
|
+
page: LIST_FIRST_PAGE,
|
|
129
|
+
size: ADMIN_LIST_MAX_SIZE,
|
|
129
130
|
});
|
|
130
131
|
const sameIpCount = page.data.filter((e) => e.ip === ip).length;
|
|
131
132
|
// > 1 significa que já havia um login.success deste IP antes do atual → não é novo.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ADMIN_LIST_MAX_SIZE, LIST_FIRST_PAGE } from '../pagination.js';
|
|
1
2
|
import { resolveGeoSafe } from './geo.js';
|
|
2
3
|
import { parseUserAgent } from './user_agent.js';
|
|
3
4
|
/**
|
|
@@ -30,8 +31,8 @@ export async function enrichSessionsWithContext(cfg, accountId, sessions) {
|
|
|
30
31
|
const page = await cfg.audit.list({
|
|
31
32
|
type: 'login.success',
|
|
32
33
|
subject: accountId,
|
|
33
|
-
page:
|
|
34
|
-
|
|
34
|
+
page: LIST_FIRST_PAGE,
|
|
35
|
+
size: ADMIN_LIST_MAX_SIZE,
|
|
35
36
|
});
|
|
36
37
|
const events = page.data.map((e) => ({
|
|
37
38
|
ts: toEpochSeconds(e.createdAt),
|