@adonis-agora/authkit-server 1.0.0-rc.15 → 1.0.0-rc.17

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 (46) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/build/index.d.ts +3 -3
  3. package/build/index.js +2 -1
  4. package/build/src/audit/audit_sink.d.ts +26 -19
  5. package/build/src/audit/lucid_audit_sink.js +99 -28
  6. package/build/src/commands/expire_scan_command.js +0 -2
  7. package/build/src/dev_tools/tools.js +10 -12
  8. package/build/src/host/account_export_service.js +0 -2
  9. package/build/src/host/admin_api/api_clients_controller.d.ts +6 -1
  10. package/build/src/host/admin_api/api_clients_controller.js +9 -5
  11. package/build/src/host/admin_api/api_misc_controller.d.ts +17 -7
  12. package/build/src/host/admin_api/api_misc_controller.js +10 -8
  13. package/build/src/host/admin_api/api_orgs_controller.d.ts +5 -1
  14. package/build/src/host/admin_api/api_orgs_controller.js +10 -8
  15. package/build/src/host/admin_api/api_users_controller.d.ts +15 -8
  16. package/build/src/host/admin_api/api_users_controller.js +11 -9
  17. package/build/src/host/admin_console/console_audit_controller.d.ts +14 -7
  18. package/build/src/host/admin_console/console_audit_controller.js +7 -8
  19. package/build/src/host/admin_console/console_clients_controller.d.ts +2 -1
  20. package/build/src/host/admin_console/console_clients_controller.js +5 -5
  21. package/build/src/host/admin_console/console_orgs_controller.d.ts +2 -1
  22. package/build/src/host/admin_console/console_orgs_controller.js +6 -2
  23. package/build/src/host/admin_console/console_overview_controller.d.ts +1 -1
  24. package/build/src/host/admin_console/console_overview_controller.js +4 -6
  25. package/build/src/host/admin_console/console_personal_agents_controller.d.ts +17 -20
  26. package/build/src/host/admin_console/console_personal_agents_controller.js +29 -8
  27. package/build/src/host/admin_console/console_sessions_controller.d.ts +11 -10
  28. package/build/src/host/admin_console/console_sessions_controller.js +41 -18
  29. package/build/src/host/admin_console/console_users_controller.d.ts +12 -8
  30. package/build/src/host/admin_console/console_users_controller.js +7 -12
  31. package/build/src/host/admin_stats_service.js +4 -4
  32. package/build/src/host/bearer_account.d.ts +8 -0
  33. package/build/src/host/bearer_account.js +11 -0
  34. package/build/src/host/list_api.d.ts +118 -0
  35. package/build/src/host/list_api.js +236 -0
  36. package/build/src/host/login_attempt.js +0 -2
  37. package/build/src/host/login_notify.js +1 -2
  38. package/build/src/host/session_context.js +1 -2
  39. package/build/src/host/ui-dist/assets/index-SteA4yAA.js +67 -0
  40. package/build/src/host/ui-dist/index.html +1 -1
  41. package/build/src/pagination.d.ts +5 -31
  42. package/build/src/pagination.js +5 -12
  43. package/build/src/provider/oidc_service.d.ts +8 -0
  44. package/build/src/provider/oidc_service.js +11 -0
  45. package/package.json +4 -3
  46. package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +0 -67
@@ -1,8 +1,8 @@
1
1
  import '../augmentations.js';
2
- import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
3
2
  import { rolesManagedExternally } from '../account_roles.js';
4
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
5
4
  import { adminUserCreateValidator, adminUserUpdateValidator } from '../admin_validators.js';
5
+ import { listAccountsPage, withListQuery } from '../list_api.js';
6
6
  import { resolveRuntimeSettings } from '../runtime_settings.js';
7
7
  import { enrichSessionsWithContext } from '../session_context.js';
8
8
  import { AdminUsersService } from './admin_users_service.js';
@@ -28,16 +28,18 @@ async function ctxBits(ctx) {
28
28
  * envelope `{ error: { code, message } }`. Toda escrita audita com `actor: 'admin-api'`.
29
29
  */
30
30
  export default class ApiUsersController {
31
- /** GET /users — `{ meta: { page, size, total }, data }`, com busca por e-mail. */
31
+ /**
32
+ * GET /users — `?search=&sort=email&page=&size=` →
33
+ * `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`.
34
+ */
32
35
  async index(ctx) {
33
36
  const { cfg } = await ctxBits(ctx);
34
- const search = ctx.request.input('search', '').trim();
35
- const page = parseListPage(ctx.request.input('page'));
36
- const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
37
- const result = await cfg.accountStore.listAccounts({ search, page, size });
38
- const users = new AdminUsersService(cfg);
39
- const data = await Promise.all(result.data.map(async (u) => userDtoWithRoles(cfg, u, await users.isDisabled(u.id))));
40
- return { meta: { page, size, total: result.total }, data };
37
+ return withListQuery(ctx, async () => {
38
+ const result = await listAccountsPage(cfg.accountStore, ctx.request.qs());
39
+ const users = new AdminUsersService(cfg);
40
+ const data = await Promise.all(result.data.map(async (u) => userDtoWithRoles(cfg, u, await users.isDisabled(u.id))));
41
+ return { ...result, data };
42
+ });
41
43
  }
42
44
  /** GET /users/:id */
43
45
  async show(ctx) {
@@ -3,17 +3,24 @@ 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=&size= → `{ meta: { page, size, total }, data }`
6
+ * GET {prefix}/api/audit?filter[type]=&filter[subject]=&after=&before=&first=&last=
7
+ * → `{ data, meta: { nextCursor, prevCursor, hasNext, hasPrev } }`, newest first
7
8
  *
8
9
  * 404 honesto (`capability_unsupported`) quando o sink não suporta consulta.
9
10
  */
10
11
  export default class ConsoleAuditController {
11
12
  index(ctx: HttpContext): Promise<void | {
12
- meta: {
13
- page: number;
14
- size: number;
15
- total: any;
16
- };
17
- data: any;
13
+ meta: import("@adonis-agora/filter").CursorPageMeta;
14
+ data: {
15
+ id: string;
16
+ type: "account.deleted" | "account.expiration_warned" | "account.expired_login_blocked" | "account.exported" | "account.extension.action" | "account.extension.redirect" | "account.locked" | "account.signed_out_all" | "agent.allowed" | "agent.blocked" | "agent.delegation_approved" | "agent.delegation_denied" | "agent.grant_revoked" | "agent.grant_revoked_by_admin" | "agent.registry_removed" | "bot_protection.rejected" | "client.created" | "client.deleted" | "client.disabled" | "client.enabled" | "client.secret_regenerated" | "client.updated" | "email.change_requested" | "email.changed" | "email_change.cancelled" | "email_change.confirmed" | "email_change.requested" | "email_verification.consumed" | "email_verification.issued" | "grant.revoked_by_user" | "impersonation" | "impersonation.panel_viewed" | "impersonation.stopped" | "keys.rotated" | "login.failure" | "login.magic_link_sent" | "login.new_device" | "login.new_ip_notified" | "login.otp_failed" | "login.otp_invalidated" | "login.otp_sent" | "login.otp_verified" | "login.success" | "maintenance.disabled" | "maintenance.enabled" | "mfa.disabled" | "mfa.enabled" | "mfa.recovery_codes_regenerated" | "organization.created" | "organization.deactivated" | "organization.deleted" | "organization.invitation_accepted" | "organization.invitation_revoked" | "organization.invitation_sent" | "organization.member_added" | "organization.member_removed" | "organization.member_role_changed" | "organization.member_role_updated" | "organization.switched" | "organization.updated" | "otp.locked" | "otp.unlock_failed" | "otp.unlocked" | "passkey.registered" | "passkey.removed" | "password.changed" | "password.expired_change_forced" | "password.rehashed" | "password_reset.consumed" | "password_reset.issued" | "pat.issued" | "pat.revoked" | "pat.used" | "profile.updated" | "roles_catalog.updated" | "security_notice.sent" | "service_tenant.token_issued" | "session.revoked" | "session.revoked_all" | "session.single_enforced" | "settings.updated" | "signup" | "sudo.confirmed" | "trusted_device.revoked" | "user.created" | "user.deleted" | "user.disabled" | "user.enabled" | "user.password_reset_sent";
17
+ accountId: string | null;
18
+ email: string | null;
19
+ clientId: string | null;
20
+ actorId: string | null;
21
+ ip: string | null;
22
+ metadata: Record<string, unknown> | null;
23
+ createdAt: string | null;
24
+ }[];
18
25
  }>;
19
26
  }
@@ -1,10 +1,11 @@
1
1
  import '../augmentations.js';
2
- import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
3
2
  import { apiError, auditDto } from '../admin_api/dto.js';
3
+ import { listAuditPage, withListQuery } from '../list_api.js';
4
4
  /**
5
5
  * Endpoints JSON do log de auditoria do console admin React.
6
6
  *
7
- * GET {prefix}/api/audit?type=&page=&size= → `{ meta: { page, size, total }, data }`
7
+ * GET {prefix}/api/audit?filter[type]=&filter[subject]=&after=&before=&first=&last=
8
+ * → `{ data, meta: { nextCursor, prevCursor, hasNext, hasPrev } }`, newest first
8
9
  *
9
10
  * 404 honesto (`capability_unsupported`) quando o sink não suporta consulta.
10
11
  */
@@ -16,11 +17,9 @@ export default class ConsoleAuditController {
16
17
  if (!sink || typeof sink.list !== 'function') {
17
18
  return ctx.response.notFound(apiError('capability_unsupported', 'O sink de auditoria configurado não suporta consulta.'));
18
19
  }
19
- const page = parseListPage(ctx.request.input('page'));
20
- const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
21
- const type = ctx.request.input('type')?.trim() || undefined;
22
- const subject = ctx.request.input('subject')?.trim() || undefined;
23
- const result = await sink.list({ page, size, type, subject });
24
- return { meta: { page, size, total: result.total }, data: result.data.map(auditDto) };
20
+ return withListQuery(ctx, async () => {
21
+ const page = await listAuditPage(sink, ctx.request.qs());
22
+ return { ...page, data: page.data.map(auditDto) };
23
+ });
25
24
  }
26
25
  }
@@ -15,7 +15,8 @@ import type { HttpContext } from '@adonisjs/core/http';
15
15
  export default class ConsoleClientsController {
16
16
  #private;
17
17
  /** GET {prefix}/api/clients */
18
- index(ctx: HttpContext): Promise<{
18
+ index(ctx: HttpContext): Promise<void | {
19
+ meta: import("@adonis-agora/filter").OffsetPageMeta;
19
20
  data: {
20
21
  clientId: string;
21
22
  confidential: boolean;
@@ -3,6 +3,7 @@ import { apiError, clientDto, createdClientDto } from '../admin_api/dto.js';
3
3
  import { AdminClientsService } from '../admin_clients_service.js';
4
4
  import { clientCreateInput, clientInputValidator, clientPartialInput, } from '../admin_validators.js';
5
5
  import { ClientMetadataError } from '../client_metadata.js';
6
+ import { listClientsPage, withListQuery } from '../list_api.js';
6
7
  /**
7
8
  * Metadata incoerente com o tipo de client (ex.: nativo com secret, redirect
8
9
  * `myapp://` num client web) — 422, igual a um input que falha no Vine.
@@ -27,11 +28,10 @@ export default class ConsoleClientsController {
27
28
  async index(ctx) {
28
29
  const service = await ctx.containerResolver.make('authkit.server');
29
30
  const svc = new AdminClientsService(service);
30
- if (!svc.canList) {
31
- return { data: [], canList: false };
32
- }
33
- const clients = await svc.list();
34
- return { data: clients.map(clientDto), canList: true };
31
+ return withListQuery(ctx, async () => {
32
+ const page = await listClientsPage(svc.canList ? await svc.list() : [], ctx.request.qs());
33
+ return { ...page, data: page.data.map(clientDto), canList: svc.canList };
34
+ });
35
35
  }
36
36
  /** GET {prefix}/api/clients/:id */
37
37
  async show(ctx) {
@@ -4,7 +4,7 @@ import type { HttpContext } from '@adonisjs/core/http';
4
4
  * Endpoints JSON de organizações do console admin React.
5
5
  * 404 honesto quando o store não suporta organizações (`capability_unsupported`).
6
6
  *
7
- * GET {prefix}/api/orgs → lista orgs com contagem de membros
7
+ * GET {prefix}/api/orgs?search=&sort=&page=&size= → página de orgs com contagem de membros
8
8
  * POST {prefix}/api/orgs → cria uma org
9
9
  * GET {prefix}/api/orgs/:id → detalhe da org (membros + convites)
10
10
  * PATCH {prefix}/api/orgs/:id → edita nome/slug/logo
@@ -22,6 +22,7 @@ export default class ConsoleOrgsController {
22
22
  private settings;
23
23
  /** GET {prefix}/api/orgs */
24
24
  index(ctx: HttpContext): Promise<void | {
25
+ meta: import("@adonis-agora/filter").OffsetPageMeta;
25
26
  data: {
26
27
  id: string;
27
28
  name: string;
@@ -3,13 +3,14 @@ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
3
3
  import { AdminOrgsService } from '../admin_api/admin_orgs_service.js';
4
4
  import { apiError, orgDetailDto, orgDto } from '../admin_api/dto.js';
5
5
  import { orgAddMemberValidator, orgCreateValidator, orgInvitationValidator, orgMemberRoleValidator, orgUpdateValidator, } from '../admin_validators.js';
6
+ import { listOrgsPage, withListQuery } from '../list_api.js';
6
7
  import { authkitOrigin } from '../origin.js';
7
8
  import { resolveRuntimeSettings } from '../runtime_settings.js';
8
9
  /**
9
10
  * Endpoints JSON de organizações do console admin React.
10
11
  * 404 honesto quando o store não suporta organizações (`capability_unsupported`).
11
12
  *
12
- * GET {prefix}/api/orgs → lista orgs com contagem de membros
13
+ * GET {prefix}/api/orgs?search=&sort=&page=&size= → página de orgs com contagem de membros
13
14
  * POST {prefix}/api/orgs → cria uma org
14
15
  * GET {prefix}/api/orgs/:id → detalhe da org (membros + convites)
15
16
  * PATCH {prefix}/api/orgs/:id → edita nome/slug/logo
@@ -46,7 +47,10 @@ export default class ConsoleOrgsController {
46
47
  if (!Array.isArray(result)) {
47
48
  return ctx.response.notFound(apiError('capability_unsupported', 'O store não suporta organizações.'));
48
49
  }
49
- return { data: result.map((o) => orgDto(o)) };
50
+ return withListQuery(ctx, async () => {
51
+ const page = await listOrgsPage(result, ctx.request.qs());
52
+ return { ...page, data: page.data.map((o) => orgDto(o)) };
53
+ });
50
54
  }
51
55
  /** POST {prefix}/api/orgs */
52
56
  async store(ctx) {
@@ -6,7 +6,7 @@ import type { HttpContext } from '@adonisjs/core/http';
6
6
  * Métricas-resumo do IdP para o dashboard do console React. Reutiliza
7
7
  * `computeAdminStats` (mesma fonte do console Edge) e adiciona:
8
8
  * - `clientsCount` — número de clients no adapter (0 quando o adapter não enumera)
9
- * - `auditTotal` — total de eventos de auditoria (0 quando sink não suporta)
9
+ * - `auditTotal` — total de eventos de auditoria (`null` quando o sink não conta)
10
10
  * - `recentEvents` — últimos 5 eventos de auditoria ([] quando não suporta)
11
11
  */
12
12
  export default class ConsoleOverviewController {
@@ -1,5 +1,4 @@
1
1
  import '../augmentations.js';
2
- import { LIST_FIRST_PAGE } from '../../pagination.js';
3
2
  import { AdminClientsService } from '../admin_clients_service.js';
4
3
  import { AdminSessionsService } from '../admin_sessions_service.js';
5
4
  import { computeAdminStats } from '../admin_stats_service.js';
@@ -9,7 +8,7 @@ import { computeAdminStats } from '../admin_stats_service.js';
9
8
  * Métricas-resumo do IdP para o dashboard do console React. Reutiliza
10
9
  * `computeAdminStats` (mesma fonte do console Edge) e adiciona:
11
10
  * - `clientsCount` — número de clients no adapter (0 quando o adapter não enumera)
12
- * - `auditTotal` — total de eventos de auditoria (0 quando sink não suporta)
11
+ * - `auditTotal` — total de eventos de auditoria (`null` quando o sink não conta)
13
12
  * - `recentEvents` — últimos 5 eventos de auditoria ([] quando não suporta)
14
13
  */
15
14
  export default class ConsoleOverviewController {
@@ -20,9 +19,8 @@ export default class ConsoleOverviewController {
20
19
  const stats = await computeAdminStats(cfg, sessions);
21
20
  const clients = new AdminClientsService(service);
22
21
  const clientsCount = clients.canList ? (await clients.list()).length : 0;
23
- const recentResult = typeof cfg.audit?.list === 'function'
24
- ? await cfg.audit.list({ page: LIST_FIRST_PAGE, size: 5 })
25
- : null;
22
+ const recentResult = typeof cfg.audit?.list === 'function' ? await cfg.audit.list({ size: 5 }) : null;
23
+ const auditTotal = typeof cfg.audit?.count === 'function' ? await cfg.audit.count({}) : null;
26
24
  return {
27
25
  usersTotal: stats.totalUsers,
28
26
  activeSessions: stats.activeSessions,
@@ -34,7 +32,7 @@ export default class ConsoleOverviewController {
34
32
  windowDays: stats.windowDays,
35
33
  auditSupported: stats.auditSupported,
36
34
  clientsCount,
37
- auditTotal: recentResult?.total ?? 0,
35
+ auditTotal,
38
36
  recentEvents: (recentResult?.data ?? []).map((e) => ({
39
37
  id: e.id,
40
38
  type: e.type,
@@ -44,26 +44,23 @@ export default class ConsolePersonalAgentsController {
44
44
  };
45
45
  agents: AgentEntry[];
46
46
  }>;
47
- /** GET {prefix}/api/personal-agents/connections */
48
- connections(ctx: HttpContext): Promise<void | {
49
- meta: {
50
- page: number;
51
- size: number;
52
- total: number;
53
- };
54
- data: {
55
- id: string;
56
- protocol: AgentProtocolId;
57
- clientId: string;
58
- agentName: string;
59
- accountId: string;
60
- accountEmail: string | null;
61
- scopes: string[];
62
- createdAt: string;
63
- updatedAt: string;
64
- expiresAt: string;
65
- }[];
66
- }>;
47
+ /**
48
+ * GET {prefix}/api/personal-agents/connections — live grants, newest first:
49
+ * `?filter[protocol]=&filter[clientId]=&filter[accountId]=&sort=-createdAt&page=&size=` →
50
+ * `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`.
51
+ */
52
+ connections(ctx: HttpContext): Promise<void | import("@adonis-agora/filter").OffsetPage<{
53
+ id: string;
54
+ protocol: AgentProtocolId;
55
+ clientId: string;
56
+ agentName: string;
57
+ accountId: string;
58
+ accountEmail: string | null;
59
+ scopes: string[];
60
+ createdAt: string;
61
+ updatedAt: string;
62
+ expiresAt: string;
63
+ }>>;
67
64
  /** POST {prefix}/api/personal-agents/connections/:grantId/revoke */
68
65
  revokeConnection(ctx: HttpContext): Promise<void | {}>;
69
66
  /**
@@ -1,10 +1,11 @@
1
1
  import '../augmentations.js';
2
+ import { InvalidColumnFilterError, offsetPage } from '@adonis-agora/filter';
2
3
  import { displayNameFor } from '../../agents/agent_identity.js';
3
4
  import { parseScope } from '../../agents/config.js';
4
5
  import { personalAgentsFor } from '../../agents/runtime.js';
5
- import { parseListPage, parseListSize } from '../../pagination.js';
6
6
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
7
7
  import { apiError } from '../admin_api/dto.js';
8
+ import { ConnectionListFilter, listQueryError, readListFilter, readPage, readSort, } from '../list_api.js';
8
9
  import { resolveRuntimeSettings } from '../runtime_settings.js';
9
10
  import { requireSudo } from '../sudo_mode.js';
10
11
  /** Mesmo gate de sudo das outras ações destrutivas do console (sessões, chaves). */
@@ -182,19 +183,39 @@ export default class ConsolePersonalAgentsController {
182
183
  agents,
183
184
  };
184
185
  }
185
- /** GET {prefix}/api/personal-agents/connections */
186
+ /**
187
+ * GET {prefix}/api/personal-agents/connections — live grants, newest first:
188
+ * `?filter[protocol]=&filter[clientId]=&filter[accountId]=&sort=-createdAt&page=&size=` →
189
+ * `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`.
190
+ */
186
191
  async connections(ctx) {
187
192
  const runtime = await runtimeFor(ctx);
188
193
  if (!runtime)
189
194
  return notSupported(ctx);
190
195
  const qs = ctx.request.qs();
191
- const page = parseListPage(qs.page);
192
- const size = parseListSize(qs.size);
193
- const protocol = qs.protocol && enabledProtocols(runtime).includes(qs.protocol) ? qs.protocol : undefined;
196
+ let query;
197
+ try {
198
+ const f = await readListFilter(qs, ConnectionListFilter);
199
+ readSort(qs, { sortable: ['createdAt'], default: '-createdAt', fixed: true });
200
+ const protocol = f.first('protocol');
201
+ if (protocol !== undefined && !enabledProtocols(runtime).includes(protocol)) {
202
+ throw new InvalidColumnFilterError(`Unknown protocol: "${protocol}".`);
203
+ }
204
+ query = {
205
+ ...readPage(qs),
206
+ ...(protocol !== undefined && { protocol }),
207
+ ...(f.first('clientId') !== undefined && { clientId: f.first('clientId') }),
208
+ ...(f.first('accountId') !== undefined && { accountId: f.first('accountId') }),
209
+ };
210
+ }
211
+ catch (error) {
212
+ return listQueryError(ctx, error);
213
+ }
214
+ const { page, size, protocol } = query;
194
215
  const filter = {
195
216
  idPrefix: protocol ? grantPrefixOf(runtime, protocol) : undefined,
196
- clientId: qs.clientId?.trim() || undefined,
197
- accountId: qs.accountId?.trim() || undefined,
217
+ clientId: query.clientId,
218
+ accountId: query.accountId,
198
219
  };
199
220
  const now = new Date();
200
221
  const [total, rows] = await Promise.all([
@@ -226,7 +247,7 @@ export default class ConsolePersonalAgentsController {
226
247
  expiresAt: g.expiresAt.toISOString(),
227
248
  };
228
249
  }));
229
- return { meta: { page, size, total }, data };
250
+ return offsetPage(data, { total, page, size });
230
251
  }
231
252
  /** POST {prefix}/api/personal-agents/connections/:grantId/revoke */
232
253
  async revokeConnection(ctx) {
@@ -3,7 +3,7 @@ import type { HttpContext } from '@adonisjs/core/http';
3
3
  /**
4
4
  * Endpoints JSON de sessões/grants do console admin React.
5
5
  *
6
- * GET {prefix}/api/sessions?accountId= → lista sessões + grants de uma conta (ou global)
6
+ * GET {prefix}/api/sessions?filter[accountId]=&search=&sort=&page=&size= → página de sessões ativas
7
7
  * POST {prefix}/api/sessions/revoke-all → revoga todas as sessões de ?accountId=
8
8
  * GET {prefix}/api/users/:id/sessions → lista sessões + grants de um usuário específico
9
9
  * POST {prefix}/api/users/:id/revoke-sessions → revoga todas as sessões de um usuário específico
@@ -14,16 +14,15 @@ export default class ConsoleSessionsController {
14
14
  * Retorna o shape UserSessionsResult: { supported, truncated, sessions, grants }.
15
15
  */
16
16
  private listForAccount;
17
- /** GET {prefix}/api/sessions?accountId= */
17
+ /**
18
+ * GET {prefix}/api/sessions — active sessions, one page:
19
+ * `?filter[accountId]=&search=&sort=-loginTs|email&page=&size=` →
20
+ * `{ data, meta, supported, truncated, grants }`. `search` matches the email or the IP;
21
+ * `grants` lists the account's grants when `filter[accountId]` is set (`[]` otherwise).
22
+ */
18
23
  index(ctx: HttpContext): Promise<void | {
19
- supported: boolean;
20
- sessions: never[];
21
- grants: never[];
22
- truncated?: undefined;
23
- } | {
24
- supported: boolean;
25
- truncated: boolean;
26
- sessions: {
24
+ meta: import("@adonis-agora/filter").OffsetPageMeta;
25
+ data: {
27
26
  id: string;
28
27
  accountId: string;
29
28
  email: string | null;
@@ -35,6 +34,8 @@ export default class ConsoleSessionsController {
35
34
  ip: string | null;
36
35
  location: string | null;
37
36
  }[];
37
+ supported: boolean;
38
+ truncated: boolean;
38
39
  grants: {
39
40
  id: string;
40
41
  accountId: string;
@@ -3,6 +3,7 @@ import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
3
3
  import { apiError, grantDto, sessionDto } from '../admin_api/dto.js';
4
4
  import { AdminSessionsService } from '../admin_sessions_service.js';
5
5
  import { sessionAccountValidator } from '../admin_validators.js';
6
+ import { pageRows, readListFilter, readSearch, readSort, SessionListFilter, searchRows, sortRows, withListQuery, } from '../list_api.js';
6
7
  import { resolveRuntimeSettings } from '../runtime_settings.js';
7
8
  import { enrichSessionsWithContext } from '../session_context.js';
8
9
  import { requireSudo } from '../sudo_mode.js';
@@ -25,7 +26,7 @@ async function gateSudo(ctx) {
25
26
  /**
26
27
  * Endpoints JSON de sessões/grants do console admin React.
27
28
  *
28
- * GET {prefix}/api/sessions?accountId= → lista sessões + grants de uma conta (ou global)
29
+ * GET {prefix}/api/sessions?filter[accountId]=&search=&sort=&page=&size= → página de sessões ativas
29
30
  * POST {prefix}/api/sessions/revoke-all → revoga todas as sessões de ?accountId=
30
31
  * GET {prefix}/api/users/:id/sessions → lista sessões + grants de um usuário específico
31
32
  * POST {prefix}/api/users/:id/revoke-sessions → revoga todas as sessões de um usuário específico
@@ -55,26 +56,48 @@ export default class ConsoleSessionsController {
55
56
  grants: grants.map(grantDto),
56
57
  };
57
58
  }
58
- /** GET {prefix}/api/sessions?accountId= */
59
+ /**
60
+ * GET {prefix}/api/sessions — active sessions, one page:
61
+ * `?filter[accountId]=&search=&sort=-loginTs|email&page=&size=` →
62
+ * `{ data, meta, supported, truncated, grants }`. `search` matches the email or the IP;
63
+ * `grants` lists the account's grants when `filter[accountId]` is set (`[]` otherwise).
64
+ */
59
65
  async index(ctx) {
60
66
  const service = await ctx.containerResolver.make('authkit.server');
61
- const accountId = ctx.request.input('accountId', '').trim();
67
+ const cfg = service.config;
62
68
  const adminSvc = new AdminSessionsService(service);
63
- if (!adminSvc.canList) {
64
- return { supported: false, sessions: [], grants: [] };
65
- }
66
- // Sem accountId → listagem global de todas as sessões ativas
67
- if (!accountId) {
68
- const { sessions: rawSessions, truncated } = await adminSvc.listAllSessions();
69
- return {
70
- supported: true,
71
- truncated,
72
- sessions: rawSessions.map(sessionDto),
73
- grants: [],
74
- };
75
- }
76
- // Com accountId → delega ao helper por-conta
77
- return this.listForAccount(ctx, accountId);
69
+ const qs = ctx.request.qs();
70
+ return withListQuery(ctx, async () => {
71
+ const filter = await readListFilter(qs, SessionListFilter);
72
+ const sort = readSort(qs, { sortable: ['loginTs', 'email'], default: '-loginTs' });
73
+ if (!adminSvc.canList) {
74
+ return { ...pageRows([], qs), supported: false, truncated: false, grants: [] };
75
+ }
76
+ const accountId = filter.first('accountId');
77
+ let sessions;
78
+ let grants = [];
79
+ let truncated = false;
80
+ if (accountId) {
81
+ const account = await cfg.accountStore.findById(accountId);
82
+ if (!account) {
83
+ return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
84
+ }
85
+ sessions = (await enrichSessionsWithContext(cfg, accountId, await adminSvc.listSessions(accountId))).map((s) => ({ ...s, email: s.email ?? account.email }));
86
+ grants = (await adminSvc.listGrants(accountId)).map(grantDto);
87
+ }
88
+ else {
89
+ const all = await adminSvc.listAllSessions();
90
+ sessions = all.sessions;
91
+ truncated = all.truncated;
92
+ }
93
+ const found = searchRows(sessions, readSearch(qs), [(s) => s.email, (s) => s.ip]);
94
+ const sorted = sortRows(found, sort, {
95
+ loginTs: (s) => s.loginTs ?? null,
96
+ email: (s) => s.email ?? null,
97
+ });
98
+ const page = pageRows(sorted, qs);
99
+ return { ...page, data: page.data.map(sessionDto), supported: true, truncated, grants };
100
+ });
78
101
  }
79
102
  /** GET {prefix}/api/users/:id/sessions */
80
103
  async userSessions(ctx) {
@@ -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=&size= → `{ meta: { page, size, total }, data }`
6
+ * GET {prefix}/api/users?search=&sort=&page=&size= → `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`
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
@@ -19,13 +19,17 @@ import type { HttpContext } from '@adonisjs/core/http';
19
19
  export default class ConsoleUsersController {
20
20
  #private;
21
21
  /** GET {prefix}/api/users */
22
- index(ctx: HttpContext): Promise<{
23
- meta: {
24
- page: number;
25
- size: number;
26
- total: any;
27
- };
28
- data: any[];
22
+ index(ctx: HttpContext): Promise<void | {
23
+ meta: import("@adonis-agora/filter").OffsetPageMeta;
24
+ data: {
25
+ id: string;
26
+ email: string;
27
+ name: string | null;
28
+ avatarUrl: string | null;
29
+ globalRoles: string[];
30
+ disabled: boolean;
31
+ roles: string[];
32
+ }[];
29
33
  }>;
30
34
  /** GET {prefix}/api/users/:id */
31
35
  show(ctx: HttpContext): Promise<void | {
@@ -1,12 +1,12 @@
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';
4
3
  import { rolesManagedExternally } from '../account_roles.js';
5
4
  import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
6
5
  import { AdminUsersService } from '../admin_api/admin_users_service.js';
7
6
  import { apiError, grantDto, rolesManagedExternallyError, sessionDto, userDtoWithRoles, } from '../admin_api/dto.js';
8
7
  import { AdminSessionsService } from '../admin_sessions_service.js';
9
8
  import { adminUserCreateValidator, adminUserRolesValidator, adminUserUpdateValidator, } from '../admin_validators.js';
9
+ import { listAccountsPage, withListQuery } from '../list_api.js';
10
10
  import { resolveRuntimeSettings } from '../runtime_settings.js';
11
11
  import { resolveEffectiveRolesCatalog } from '../runtime_toggles.js';
12
12
  import { enrichSessionsWithContext } from '../session_context.js';
@@ -35,7 +35,7 @@ async function gateSudo(ctx) {
35
35
  /**
36
36
  * Endpoints JSON de usuários do console admin React.
37
37
  *
38
- * GET {prefix}/api/users?search=&page=&size= → `{ meta: { page, size, total }, data }`
38
+ * GET {prefix}/api/users?search=&sort=&page=&size= → `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`
39
39
  * GET {prefix}/api/users/:id → detalhe + sessões + identidades + MFA status
40
40
  * POST {prefix}/api/users → criar usuário
41
41
  * PATCH {prefix}/api/users/:id/roles → substituir roles globais
@@ -53,17 +53,12 @@ export default class ConsoleUsersController {
53
53
  async index(ctx) {
54
54
  const service = await ctx.containerResolver.make('authkit.server');
55
55
  const cfg = service.config;
56
- const search = ctx.request.input('search', '').trim();
57
- const page = parseListPage(ctx.request.input('page'));
58
- const size = parseListSize(ctx.request.input('size'), ADMIN_LIST_HTTP_DEFAULT_SIZE);
59
- const result = await cfg.accountStore.listAccounts({
60
- search,
61
- page,
62
- size,
56
+ return withListQuery(ctx, async () => {
57
+ const result = await listAccountsPage(cfg.accountStore, ctx.request.qs());
58
+ const users = new AdminUsersService(cfg);
59
+ const data = await Promise.all(result.data.map(async (u) => userDtoWithRoles(cfg, u, await users.isDisabled(u.id))));
60
+ return { ...result, data };
63
61
  });
64
- const users = new AdminUsersService(cfg);
65
- const data = await Promise.all(result.data.map(async (u) => userDtoWithRoles(cfg, u, await users.isDisabled(u.id))));
66
- return { meta: { page, size, total: result.total }, data };
67
62
  }
68
63
  /** GET {prefix}/api/users/:id */
69
64
  async show(ctx) {
@@ -68,9 +68,9 @@ export async function computeAdminStats(cfg, sessionsService) {
68
68
  async function loadEventsInWindow(audit, type, windowStartMs) {
69
69
  const out = [];
70
70
  const size = ADMIN_LIST_MAX_SIZE;
71
- let page = LIST_FIRST_PAGE;
71
+ let after;
72
72
  while (out.length < MAX_EVENTS) {
73
- const result = await audit.list({ type, page, size });
73
+ const result = await audit.list({ type, size, ...(after !== undefined && { after }) });
74
74
  if (result.data.length === 0)
75
75
  break;
76
76
  let allBelowWindow = true;
@@ -88,9 +88,9 @@ async function loadEventsInWindow(audit, type, windowStartMs) {
88
88
  // Página inteira anterior à janela → não há mais nada relevante (ordem desc).
89
89
  if (allBelowWindow)
90
90
  break;
91
- if (result.data.length < size)
91
+ if (!result.hasNext || !result.nextCursor)
92
92
  break;
93
- page += 1;
93
+ after = result.nextCursor;
94
94
  }
95
95
  return out;
96
96
  }
@@ -1,5 +1,13 @@
1
1
  /** Registra a conta autenticada via bearer nesta request. */
2
2
  export declare function setBearerAccountId(ctx: object, accountId: string): void;
3
+ /**
4
+ * Vincula a conta de um bearer que foi autenticado FORA do `oidcBearerGuard`
5
+ * (ex.: o endpoint MCP do agent) a esta request, para que `getAccountId`,
6
+ * `realAccountId` e `impersonationState` vejam quem chamou. O chamador conta
7
+ * como "sem impersonation": qualquer impersonation bearer anterior desta request
8
+ * é esquecida.
9
+ */
10
+ export declare function bindBearerAccount(ctx: object, accountId: string): void;
3
11
  /** Esquece a conta bearer desta request (falha de autenticação). */
4
12
  export declare function clearBearerAccountId(ctx: object): void;
5
13
  /**
@@ -16,6 +16,17 @@ const bearerImpersonations = new WeakMap();
16
16
  export function setBearerAccountId(ctx, accountId) {
17
17
  bearerAccounts.set(ctx, accountId);
18
18
  }
19
+ /**
20
+ * Vincula a conta de um bearer que foi autenticado FORA do `oidcBearerGuard`
21
+ * (ex.: o endpoint MCP do agent) a esta request, para que `getAccountId`,
22
+ * `realAccountId` e `impersonationState` vejam quem chamou. O chamador conta
23
+ * como "sem impersonation": qualquer impersonation bearer anterior desta request
24
+ * é esquecida.
25
+ */
26
+ export function bindBearerAccount(ctx, accountId) {
27
+ bearerAccounts.set(ctx, accountId);
28
+ bearerImpersonations.delete(ctx);
29
+ }
19
30
  /** Esquece a conta bearer desta request (falha de autenticação). */
20
31
  export function clearBearerAccountId(ctx) {
21
32
  bearerAccounts.delete(ctx);