@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.
- package/CHANGELOG.md +41 -0
- package/build/index.d.ts +3 -3
- package/build/index.js +2 -1
- package/build/src/audit/audit_sink.d.ts +26 -19
- package/build/src/audit/lucid_audit_sink.js +99 -28
- package/build/src/commands/expire_scan_command.js +0 -2
- package/build/src/dev_tools/tools.js +10 -12
- package/build/src/host/account_export_service.js +0 -2
- package/build/src/host/admin_api/api_clients_controller.d.ts +6 -1
- package/build/src/host/admin_api/api_clients_controller.js +9 -5
- package/build/src/host/admin_api/api_misc_controller.d.ts +17 -7
- package/build/src/host/admin_api/api_misc_controller.js +10 -8
- package/build/src/host/admin_api/api_orgs_controller.d.ts +5 -1
- package/build/src/host/admin_api/api_orgs_controller.js +10 -8
- package/build/src/host/admin_api/api_users_controller.d.ts +15 -8
- package/build/src/host/admin_api/api_users_controller.js +11 -9
- package/build/src/host/admin_console/console_audit_controller.d.ts +14 -7
- package/build/src/host/admin_console/console_audit_controller.js +7 -8
- package/build/src/host/admin_console/console_clients_controller.d.ts +2 -1
- package/build/src/host/admin_console/console_clients_controller.js +5 -5
- package/build/src/host/admin_console/console_orgs_controller.d.ts +2 -1
- package/build/src/host/admin_console/console_orgs_controller.js +6 -2
- package/build/src/host/admin_console/console_overview_controller.d.ts +1 -1
- package/build/src/host/admin_console/console_overview_controller.js +4 -6
- package/build/src/host/admin_console/console_personal_agents_controller.d.ts +17 -20
- package/build/src/host/admin_console/console_personal_agents_controller.js +29 -8
- package/build/src/host/admin_console/console_sessions_controller.d.ts +11 -10
- package/build/src/host/admin_console/console_sessions_controller.js +41 -18
- package/build/src/host/admin_console/console_users_controller.d.ts +12 -8
- package/build/src/host/admin_console/console_users_controller.js +7 -12
- package/build/src/host/admin_stats_service.js +4 -4
- package/build/src/host/bearer_account.d.ts +8 -0
- package/build/src/host/bearer_account.js +11 -0
- package/build/src/host/list_api.d.ts +118 -0
- package/build/src/host/list_api.js +236 -0
- package/build/src/host/login_attempt.js +0 -2
- package/build/src/host/login_notify.js +1 -2
- package/build/src/host/session_context.js +1 -2
- package/build/src/host/ui-dist/assets/index-SteA4yAA.js +67 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/pagination.d.ts +5 -31
- package/build/src/pagination.js +5 -12
- package/build/src/provider/oidc_service.d.ts +8 -0
- package/build/src/provider/oidc_service.js +11 -0
- package/package.json +4 -3
- package/build/src/host/ui-dist/assets/index-Dq3dJVBv.js +0 -67
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @adonis-agora/authkit-server
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.17
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- [#484](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/484) [`4a9eeb4`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/4a9eeb46782e78ad06ad0c2955e49398ab21bc7e) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Lists use the `@adonis-agora/filter` wire format: `filter[field]=…`, `search`, `sort=-field`, then `page`/`size` or a cursor. A filter field, operator or sort a list does not support answers `400 { error: { code: 'invalid_list_query' } }`.
|
|
8
|
+
|
|
9
|
+
`@adonis-agora/authkit-server`:
|
|
10
|
+
|
|
11
|
+
- `GET {admin}/api/users` and Admin API `GET /users`: `?search&sort&page&size` (`sort` accepts `email` only) → `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }` (was `meta: { page, size, total }`).
|
|
12
|
+
- `GET {admin}/api/clients` and `GET /clients`: page with `?search&sort&page&size` (`sort`: `clientId`, `clientName`) → `{ data, meta, canList }`.
|
|
13
|
+
- `GET {admin}/api/orgs` and `GET /organizations`: page with `?search&sort&page&size` (`sort`: `name`, `slug`, `createdAt`, `memberCount`) → `{ data, meta }`.
|
|
14
|
+
- `GET {admin}/api/audit` and `GET /audit`: newest first, by cursor. `?filter[type]` (one or more), `?filter[subject]`, `after` / `before` + `first` / `last` → `{ data, meta: { nextCursor, prevCursor, hasNext, hasPrev } }`. `?type`, `?subject`, `page` and `size` are gone.
|
|
15
|
+
- `GET {admin}/api/sessions`: `?filter[accountId]&search&sort&page&size` (`sort`: `loginTs`, `email`; default `-loginTs`) → `{ data, meta, supported, truncated, grants }` (was `{ supported, truncated, sessions, grants }` with `?accountId`).
|
|
16
|
+
- `GET {admin}/api/personal-agents/connections`: `filter[protocol]`, `filter[clientId]`, `filter[accountId]` (were flat `?protocol=`…) → `{ data, meta }` with the offset meta above.
|
|
17
|
+
- `GET {admin}/api/overview`: `auditTotal` is `null` when the audit sink cannot count.
|
|
18
|
+
- `AuditSink.list` pages by cursor: it takes `{ type?: string | string[], subject?, size?, after?, before? }` and returns `{ data, nextCursor, prevCursor, hasNext, hasPrev }` (newest first). New optional `AuditSink.count({ type?, subject? })`. `lucidAuditSink` implements both (keyset on `createdAt`, `id`). Custom sinks with `list` must follow the new shape.
|
|
19
|
+
- The `authkit_list_audit` dev tool takes `cursor` (the previous result's `next`) instead of `page`, and no longer returns `total`.
|
|
20
|
+
- The `authkit_get_overview` dev tool returns `auditTotal: null` when the audit sink cannot count.
|
|
21
|
+
- Removed exports: `ListMeta`, `PaginatedResponse`, `parseListPage`, `parseListSize`. New exports: `listAccountsPage`, `listAuditPage`, `listClientsPage`, `listOrgsPage`, the list filter classes and `CountAuditParams`.
|
|
22
|
+
- Depends on `@adonis-agora/filter`.
|
|
23
|
+
|
|
24
|
+
`@adonis-agora/authkit-react`:
|
|
25
|
+
|
|
26
|
+
- `client.admin.users.list({ search?, sort?, page?, size? })`, `clients.list(params?)`, `orgs.list(params?)` return `OffsetPage<T>` (`meta: { total, perPage, currentPage, lastPage, firstPage }`).
|
|
27
|
+
- `client.admin.sessions.list({ accountId?, search?, sort?, page?, size? })` (was `list(accountId?)`) returns `AdminSessionListResult` (`data` instead of `sessions`).
|
|
28
|
+
- `client.admin.audit.list({ type?, subject?, after?, before?, first?, last? })` returns `CursorPageResponse<AuditEventEntry>`; `page` / `size` are gone.
|
|
29
|
+
- `useSessionsQueryOptions(params?)`, `useClientsQueryOptions(params?)` and `useOrgsQueryOptions(params?)` take the list params; `authkitKeys.admin.sessions/clients/orgs` take them too.
|
|
30
|
+
- `ListMeta` is removed; `OffsetPage`, `OffsetPageMeta`, `CursorPageResponse`, `CursorPageMeta`, `AdminListParams`, `CursorListParams`, `AdminSessionListParams` and `AdminSessionListResult` are exported. `AdminOverview.auditTotal` is `number | null`.
|
|
31
|
+
|
|
32
|
+
`@adonis-agora/authkit-sdk`:
|
|
33
|
+
|
|
34
|
+
- `users.list`, `clients.list` and `organizations.list` take `{ search?, sort?, page?, size? }` and return `OffsetPage<T>` (`clients.list` adds `canList`).
|
|
35
|
+
- `audit.list({ type?, subject?, after?, before?, first?, last? })` returns `CursorPageResponse<AuthkitAuditEvent>`.
|
|
36
|
+
- `ListMeta` is removed; `OffsetPage`, `CursorPageResponse`, `OffsetListParams`, `CursorListParams`, `ListClientsParams` and `ListOrganizationsParams` are exported.
|
|
37
|
+
|
|
38
|
+
## 1.0.0-rc.16
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- [#483](https://github.com/DavideCarvalho/adonis-agora-authkit/pull/483) [`dcfbed1`](https://github.com/DavideCarvalho/adonis-agora-authkit/commit/dcfbed164d3d79dae3f37ad70ad07b5450822887) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - The `authkit.server` service gains `bindBearerAccount(ctx, accountId)`. Code that verified an authkit access token outside `oidcBearerGuard` (for example `@adonis-agora/agent`'s dev MCP endpoint) calls it to put the caller on the request: `getAccountId(ctx)` and `realAccountId(ctx)` then return that account, and `impersonationState(ctx)` reports no impersonation. A console session on the same request still wins.
|
|
43
|
+
|
|
3
44
|
## 1.0.0-rc.15
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/build/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { personalAgentAuth, personalAgentOf, personalAgentReceipt, personalAgent
|
|
|
21
21
|
export type { BuiltinProtocolId, PactOptions, PersonalAgentProtocol, } from './src/agents/protocol.js';
|
|
22
22
|
export { pact, pactProtocol } from './src/agents/protocol.js';
|
|
23
23
|
export { type PersonalAgentRuntimeSource, personalAgentExtensionRuntime, } from './src/agents/runtime.js';
|
|
24
|
-
export type { AuditEvent, AuditEventType, AuditPage, AuditSink, ListAuditParams, StoredAuditEvent, } from './src/audit/audit_sink.js';
|
|
24
|
+
export type { AuditEvent, AuditEventType, AuditPage, AuditSink, CountAuditParams, ListAuditParams, StoredAuditEvent, } from './src/audit/audit_sink.js';
|
|
25
25
|
export { lucidAuditSink } from './src/audit/lucid_audit_sink.js';
|
|
26
26
|
export type { ConsoleAuthorize, ConsoleAuthorizeResult, ConsoleAuthorizer, ConsoleAuthorizerClass, } from './src/console_authorize.js';
|
|
27
27
|
export { authorizeByToken } from './src/console_authorize.js';
|
|
@@ -101,6 +101,7 @@ export type { ImpersonationStartErrorCode, ImpersonationState, StartImpersonatio
|
|
|
101
101
|
export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
102
102
|
export type { KeysStatus as ServerKeysStatus } from './src/host/key_rotation_actions.js';
|
|
103
103
|
export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
104
|
+
export { AuditListFilter, ConnectionListFilter, type ListQuery, listAccountsPage, listAuditPage, listClientsPage, listOrgsPage, NoListFilter, SessionListFilter, } from './src/host/list_api.js';
|
|
104
105
|
export type { OidcBearerGuardEvents, OidcBearerGuardOptions, OidcBearerGuardPolicy, } from './src/host/oidc_bearer_guard.js';
|
|
105
106
|
export { OidcBearerGuard, oidcBearerGuard } from './src/host/oidc_bearer_guard.js';
|
|
106
107
|
export type { OidcRpGuardEvents, OidcRpGuardOptions } from './src/host/oidc_rp_guard.js';
|
|
@@ -144,8 +145,7 @@ export type { ProviderIdentityClass, ProviderIdentityRow, } from './src/mixins/w
|
|
|
144
145
|
export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
|
|
145
146
|
export type { WebauthnCredentialClass, WebauthnCredentialRow, } from './src/mixins/with_webauthn_credential.js';
|
|
146
147
|
export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
|
|
147
|
-
export
|
|
148
|
-
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';
|
|
148
|
+
export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize, resolveListPage, } from './src/pagination.js';
|
|
149
149
|
export type { LegacyPasswordVerifier, PasswordConfigInput, PasswordVerifyResult, } from './src/password/password_manager.js';
|
|
150
150
|
export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
|
|
151
151
|
export type { PasswordPolicyViolation, ResolvedPasswordConfig, ResolvedPasswordPolicy, } from './src/password/policy.js';
|
package/build/index.js
CHANGED
|
@@ -73,6 +73,7 @@ export { buildImpersonationPanel } from './src/host/impersonation.js';
|
|
|
73
73
|
export { ImpersonationStartError, impersonationState, refreshAccessToken, rememberAccessToken, rememberRefreshToken, startImpersonation, stopImpersonation, } from './src/host/impersonation_session.js';
|
|
74
74
|
// Key rotation actions — shared between the Admin REST API controller and the SDK embedded driver.
|
|
75
75
|
export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
76
|
+
export { AuditListFilter, ConnectionListFilter, listAccountsPage, listAuditPage, listClientsPage, listOrgsPage, NoListFilter, SessionListFilter, } from './src/host/list_api.js';
|
|
76
77
|
// @adonisjs/auth integration (opt-in) — guard de access token (Authorization:
|
|
77
78
|
// Bearer) pra APIs chamadas por apps nativos (RFC 8252), SPAs e serviços. Par do
|
|
78
79
|
// oidcRpGuard para clientes sem sessão web; verifica in-process (issuer embarcado)
|
|
@@ -126,7 +127,7 @@ export { withCredentials } from './src/mixins/with_credentials.js';
|
|
|
126
127
|
export { withPersonalAccessToken } from './src/mixins/with_personal_access_token.js';
|
|
127
128
|
export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
|
|
128
129
|
export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
|
|
129
|
-
export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize,
|
|
130
|
+
export { ADMIN_LIST_DEFAULT_SIZE, ADMIN_LIST_HTTP_DEFAULT_SIZE, ADMIN_LIST_MAX_SIZE, clampListSize, LIST_FIRST_PAGE, normalizeListSize, resolveListPage, } from './src/pagination.js';
|
|
130
131
|
// Gerência de senha: lazy rehash + legacy verifier, política e checagem de vazamento.
|
|
131
132
|
export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
|
|
132
133
|
export { checkPasswordPolicy, DEFAULT_PWNED_TIMEOUT_MS, policyViolationParams, } from './src/password/policy.js';
|
|
@@ -43,31 +43,36 @@ export interface StoredAuditEvent extends AuditEvent {
|
|
|
43
43
|
createdAt: Date | string | null;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* What {@link AuditSink.list} returns: one page of the log, NEWEST FIRST, read by cursor.
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* The log is append-only and grows without bound, so it pages by an opaque cursor rather than
|
|
49
|
+
* by page number: `after` = the `nextCursor` of the previous page (older events), `before` =
|
|
50
|
+
* the `prevCursor` of a page (newer events). The cursor's content is the sink's business; the
|
|
51
|
+
* caller hands back what the sink returned.
|
|
52
52
|
*/
|
|
53
53
|
export interface ListAuditParams {
|
|
54
|
-
/**
|
|
55
|
-
|
|
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;
|
|
62
|
-
/** Filtra por tipo de evento exato. */
|
|
63
|
-
type?: string;
|
|
64
|
-
/** Filtra pelo subject (accountId) do evento. */
|
|
54
|
+
/** Only events of this type (or of any of these types). */
|
|
55
|
+
type?: string | readonly string[];
|
|
56
|
+
/** Only events about this account (`accountId`). */
|
|
65
57
|
subject?: string;
|
|
58
|
+
/** Events per page. Default 20. */
|
|
59
|
+
size?: number;
|
|
60
|
+
/** The page after this cursor (older events). Wins over `before`. */
|
|
61
|
+
after?: string;
|
|
62
|
+
/** The page before this cursor (newer events). */
|
|
63
|
+
before?: string;
|
|
66
64
|
}
|
|
67
|
-
/**
|
|
65
|
+
/** The filters of {@link AuditSink.count}. */
|
|
66
|
+
export type CountAuditParams = Pick<ListAuditParams, 'type' | 'subject'>;
|
|
67
|
+
/** One page of events, newest first, and the cursors around it. */
|
|
68
68
|
export interface AuditPage {
|
|
69
69
|
data: StoredAuditEvent[];
|
|
70
|
-
|
|
70
|
+
/** Cursor of the next (older) page, `null` on the last one. */
|
|
71
|
+
nextCursor: string | null;
|
|
72
|
+
/** Cursor of the previous (newer) page, `null` on the first one. */
|
|
73
|
+
prevCursor: string | null;
|
|
74
|
+
hasNext: boolean;
|
|
75
|
+
hasPrev: boolean;
|
|
71
76
|
}
|
|
72
77
|
/**
|
|
73
78
|
* Sink plugável de auditoria. Implementações devem ser best-effort: `record`
|
|
@@ -79,8 +84,10 @@ export interface AuditPage {
|
|
|
79
84
|
*/
|
|
80
85
|
export interface AuditSink {
|
|
81
86
|
record(event: AuditEvent): Promise<void>;
|
|
82
|
-
/**
|
|
87
|
+
/** One page of events, newest first (optional: only sinks that can be queried). */
|
|
83
88
|
list?(params: ListAuditParams): Promise<AuditPage>;
|
|
89
|
+
/** How many events match (optional; the admin overview shows it when present). */
|
|
90
|
+
count?(params: CountAuditParams): Promise<number>;
|
|
84
91
|
/**
|
|
85
92
|
* Anonimiza (LGPD/GDPR) os eventos de uma conta SEM apagar o histórico: remove
|
|
86
93
|
* os identificadores pessoais (`email`, `ip`) e re-escreve o `accountId` para um
|
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { decodeCursor, encodeCursor } from '@adonis-agora/filter';
|
|
3
|
+
import { DateTime } from 'luxon';
|
|
4
|
+
import { ADMIN_LIST_HTTP_DEFAULT_SIZE, normalizeListSize } from '../pagination.js';
|
|
5
|
+
/** Narrows an audit query to the requested type(s) and subject. */
|
|
6
|
+
function filtered(q, params) {
|
|
7
|
+
const types = params.type === undefined ? [] : [].concat(params.type);
|
|
8
|
+
if (types.length === 1)
|
|
9
|
+
q.where('type', types[0]);
|
|
10
|
+
else if (types.length > 1)
|
|
11
|
+
q.whereIn('type', types);
|
|
12
|
+
if (params.subject)
|
|
13
|
+
q.where('accountId', params.subject);
|
|
14
|
+
return q;
|
|
15
|
+
}
|
|
16
|
+
/** A Lucid `createdAt` (luxon), a `Date` or an ISO string, as an ISO string. */
|
|
17
|
+
function isoOf(value) {
|
|
18
|
+
if (value === null || value === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
if (typeof value.toISO === 'function')
|
|
21
|
+
return value.toISO();
|
|
22
|
+
if (value instanceof Date)
|
|
23
|
+
return value.toISOString();
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
function toStored(row) {
|
|
27
|
+
return {
|
|
28
|
+
id: String(row.id),
|
|
29
|
+
type: row.type,
|
|
30
|
+
accountId: row.accountId ?? null,
|
|
31
|
+
email: row.email ?? null,
|
|
32
|
+
clientId: row.clientId ?? null,
|
|
33
|
+
actorId: row.actorId ?? null,
|
|
34
|
+
ip: row.ip ?? null,
|
|
35
|
+
metadata: row.metadata ?? undefined,
|
|
36
|
+
createdAt: isoOf(row.createdAt),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** The row's position in the log (`createdAt`, primary key), as an opaque cursor. */
|
|
40
|
+
function encodeAuditCursor(row) {
|
|
41
|
+
return encodeCursor([isoOf(row.createdAt), row.$primaryKeyValue ?? row.id]);
|
|
42
|
+
}
|
|
43
|
+
/** A cursor back to `{ at, id }`; `null` for anything this sink did not produce. */
|
|
44
|
+
function decodeAuditCursor(cursor) {
|
|
45
|
+
const values = decodeCursor(cursor);
|
|
46
|
+
if (!values || values.length !== 2 || typeof values[0] !== 'string')
|
|
47
|
+
return null;
|
|
48
|
+
const at = DateTime.fromISO(values[0]);
|
|
49
|
+
if (!at.isValid)
|
|
50
|
+
return null;
|
|
51
|
+
const id = values[1];
|
|
52
|
+
if (typeof id !== 'string' && typeof id !== 'number')
|
|
53
|
+
return null;
|
|
54
|
+
return { at, id };
|
|
55
|
+
}
|
|
3
56
|
/**
|
|
4
57
|
* Pseudônimo estável para um accountId anonimizado: `anon:<sha256(accountId)[:16]>`.
|
|
5
58
|
* Mantém os eventos de uma conta deletada correlacionáveis ENTRE SI (mesmo
|
|
@@ -36,35 +89,53 @@ export function lucidAuditSink(Model) {
|
|
|
36
89
|
}
|
|
37
90
|
},
|
|
38
91
|
async list(params) {
|
|
39
|
-
|
|
92
|
+
// The primary key breaks ties between events written in the same instant.
|
|
93
|
+
const pk = Model.primaryKey ?? 'id';
|
|
40
94
|
const size = normalizeListSize(params.size, ADMIN_LIST_HTTP_DEFAULT_SIZE);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
95
|
+
const after = params.after ? decodeAuditCursor(params.after) : null;
|
|
96
|
+
const before = !after && params.before ? decodeAuditCursor(params.before) : null;
|
|
97
|
+
const backward = before !== null;
|
|
98
|
+
const seek = after ?? before;
|
|
99
|
+
// Newest first: (createdAt, id) DESC. Going backward the seek and the order flip, and the
|
|
100
|
+
// rows are flipped back below. One extra row tells whether a further page exists.
|
|
101
|
+
const q = filtered(Model.query(), params);
|
|
102
|
+
if (seek) {
|
|
103
|
+
const op = backward ? '>' : '<';
|
|
104
|
+
// Formatted the way Lucid writes `createdAt` for this dialect: nested `where` callbacks
|
|
105
|
+
// do not convert a luxon value, and the comparison must be against the stored form.
|
|
106
|
+
const format = q.client?.dialect?.dateTimeFormat;
|
|
107
|
+
const at = format ? seek.at.toFormat(format) : seek.at.toJSDate();
|
|
108
|
+
q.where((w) => {
|
|
109
|
+
w.where('createdAt', op, at).orWhere((eq) => {
|
|
110
|
+
eq.where('createdAt', at).where(pk, op, seek.id);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const dir = backward ? 'asc' : 'desc';
|
|
115
|
+
const rows = await q
|
|
116
|
+
.orderBy('createdAt', dir)
|
|
117
|
+
.orderBy(pk, dir)
|
|
118
|
+
.limit(size + 1);
|
|
119
|
+
const hasMore = rows.length > size;
|
|
120
|
+
let page = hasMore ? rows.slice(0, size) : rows;
|
|
121
|
+
if (backward)
|
|
122
|
+
page = page.slice().reverse();
|
|
123
|
+
const data = page.map(toStored);
|
|
124
|
+
const first = page[0];
|
|
125
|
+
const last = page[page.length - 1];
|
|
126
|
+
const hasNext = backward ? seek !== null : hasMore;
|
|
127
|
+
const hasPrev = backward ? hasMore : seek !== null;
|
|
128
|
+
return {
|
|
129
|
+
data,
|
|
130
|
+
nextCursor: hasNext && last ? encodeAuditCursor(last) : null,
|
|
131
|
+
prevCursor: hasPrev && first ? encodeAuditCursor(first) : null,
|
|
132
|
+
hasNext,
|
|
133
|
+
hasPrev,
|
|
48
134
|
};
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
.offset((page - 1) * size)
|
|
54
|
-
.limit(size);
|
|
55
|
-
const data = rows.map((row) => ({
|
|
56
|
-
id: String(row.id),
|
|
57
|
-
type: row.type,
|
|
58
|
-
accountId: row.accountId ?? null,
|
|
59
|
-
email: row.email ?? null,
|
|
60
|
-
clientId: row.clientId ?? null,
|
|
61
|
-
actorId: row.actorId ?? null,
|
|
62
|
-
ip: row.ip ?? null,
|
|
63
|
-
metadata: row.metadata ?? undefined,
|
|
64
|
-
// createdAt é um luxon DateTime no Lucid; normaliza para ISO string.
|
|
65
|
-
createdAt: row.createdAt?.toISO?.() ?? row.createdAt ?? null,
|
|
66
|
-
}));
|
|
67
|
-
return { data, total };
|
|
135
|
+
},
|
|
136
|
+
async count(params) {
|
|
137
|
+
const result = await filtered(Model.query(), params).count('* as total');
|
|
138
|
+
return Number(result[0]?.$extras?.total ?? 0);
|
|
68
139
|
},
|
|
69
140
|
/**
|
|
70
141
|
* Anonimiza (LGPD/GDPR) as linhas de audit cujo subject (`accountId`) OU ator
|
|
@@ -150,7 +150,6 @@ async function getLastLoginMs(audit, accountId) {
|
|
|
150
150
|
const result = await audit.list({
|
|
151
151
|
type: 'login.success',
|
|
152
152
|
subject: accountId,
|
|
153
|
-
page: LIST_FIRST_PAGE,
|
|
154
153
|
size: 1,
|
|
155
154
|
});
|
|
156
155
|
if (result.data.length === 0)
|
|
@@ -174,7 +173,6 @@ async function wasWarnedRecently(audit, accountId, warnDays) {
|
|
|
174
173
|
const result = await audit.list({
|
|
175
174
|
type: 'account.expiration_warned',
|
|
176
175
|
subject: accountId,
|
|
177
|
-
page: LIST_FIRST_PAGE,
|
|
178
176
|
size: 1,
|
|
179
177
|
});
|
|
180
178
|
if (result.data.length === 0)
|
|
@@ -98,9 +98,8 @@ export function authkitDevTools(deps = {}) {
|
|
|
98
98
|
const stats = await computeAdminStats(cfg, sessions(svc));
|
|
99
99
|
const clientSvc = clients(svc);
|
|
100
100
|
const clientsCount = clientSvc.canList ? (await clientSvc.list()).length : 0;
|
|
101
|
-
const recent = typeof cfg.audit?.list === 'function'
|
|
102
|
-
|
|
103
|
-
: null;
|
|
101
|
+
const recent = typeof cfg.audit?.list === 'function' ? await cfg.audit.list({ size: 5 }) : null;
|
|
102
|
+
const auditTotal = typeof cfg.audit?.count === 'function' ? await cfg.audit.count({}) : null;
|
|
104
103
|
return {
|
|
105
104
|
usersTotal: stats.totalUsers,
|
|
106
105
|
activeSessions: stats.activeSessions,
|
|
@@ -112,7 +111,7 @@ export function authkitDevTools(deps = {}) {
|
|
|
112
111
|
windowDays: stats.windowDays,
|
|
113
112
|
auditSupported: stats.auditSupported,
|
|
114
113
|
clientsCount,
|
|
115
|
-
auditTotal
|
|
114
|
+
auditTotal,
|
|
116
115
|
recentEvents: (recent?.data ?? []).map(auditDto),
|
|
117
116
|
};
|
|
118
117
|
},
|
|
@@ -290,13 +289,13 @@ export function authkitDevTools(deps = {}) {
|
|
|
290
289
|
{
|
|
291
290
|
name: 'authkit_list_audit',
|
|
292
291
|
kind: 'read',
|
|
293
|
-
description: 'The audit log, newest first: event type, account, actor, client, IP, metadata, time. Filter by `type` (e.g. "login.failure") and `subject` (an account id). Returns { supported, items,
|
|
292
|
+
description: 'The audit log, newest first: event type, account, actor, client, IP, metadata, time. Filter by `type` (e.g. "login.failure") and `subject` (an account id). Pass the `next` of a result as `cursor` for the following (older) page. Returns { supported, items, next }.',
|
|
294
293
|
input: {
|
|
295
294
|
type: 'object',
|
|
296
295
|
properties: {
|
|
297
296
|
type: { type: 'string', description: 'Event type, e.g. "login.success".' },
|
|
298
297
|
subject: { type: 'string', description: 'Account id the events are about.' },
|
|
299
|
-
page,
|
|
298
|
+
cursor: { type: 'string', description: 'The `next` of the previous page.' },
|
|
300
299
|
limit,
|
|
301
300
|
},
|
|
302
301
|
},
|
|
@@ -304,20 +303,19 @@ export function authkitDevTools(deps = {}) {
|
|
|
304
303
|
const cfg = (await service(call.ctx)).config;
|
|
305
304
|
const sink = cfg.audit;
|
|
306
305
|
if (!sink || typeof sink.list !== 'function') {
|
|
307
|
-
return { supported: false, items: [],
|
|
306
|
+
return { supported: false, items: [], next: null };
|
|
308
307
|
}
|
|
309
|
-
const
|
|
308
|
+
const cursor = text(input, 'cursor');
|
|
310
309
|
const result = await sink.list({
|
|
311
|
-
|
|
312
|
-
size: p.limit,
|
|
310
|
+
size: pageOf(input).limit,
|
|
313
311
|
type: text(input, 'type'),
|
|
314
312
|
subject: text(input, 'subject'),
|
|
313
|
+
...(cursor !== undefined && { after: cursor }),
|
|
315
314
|
});
|
|
316
315
|
return {
|
|
317
316
|
supported: true,
|
|
318
317
|
items: result.data.map(auditDto),
|
|
319
|
-
|
|
320
|
-
next: nextPage(p.page, p.limit, result.total),
|
|
318
|
+
next: result.hasNext ? result.nextCursor : null,
|
|
321
319
|
};
|
|
322
320
|
},
|
|
323
321
|
},
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { supportsOrganizations, supportsPasskeys, supportsProviderIdentity, } from '../accounts/account_store.js';
|
|
2
|
-
import { LIST_FIRST_PAGE } from '../pagination.js';
|
|
3
2
|
import { AdminSessionsService } from './admin_sessions_service.js';
|
|
4
3
|
/** Limite de eventos de audit incluídos no export (evita payloads gigantes). */
|
|
5
4
|
const AUDIT_EXPORT_SIZE = 1000;
|
|
@@ -103,7 +102,6 @@ export class AccountExportService {
|
|
|
103
102
|
try {
|
|
104
103
|
const page = await cfg.audit.list({
|
|
105
104
|
subject: accountId,
|
|
106
|
-
page: LIST_FIRST_PAGE,
|
|
107
105
|
size: AUDIT_EXPORT_SIZE,
|
|
108
106
|
});
|
|
109
107
|
auditLog = page.data.map((e) => ({
|
|
@@ -7,7 +7,12 @@ import type { HttpContext } from '@adonisjs/core/http';
|
|
|
7
7
|
*/
|
|
8
8
|
export default class ApiClientsController {
|
|
9
9
|
#private;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* GET /clients — `?search=&sort=clientId|clientName&page=&size=` →
|
|
12
|
+
* `{ data, meta: { total, perPage, currentPage, lastPage, firstPage }, canList }`.
|
|
13
|
+
*/
|
|
14
|
+
index(ctx: HttpContext): Promise<void | {
|
|
15
|
+
meta: import("@adonis-agora/filter").OffsetPageMeta;
|
|
11
16
|
data: {
|
|
12
17
|
clientId: string;
|
|
13
18
|
confidential: boolean;
|
|
@@ -2,6 +2,7 @@ import '../augmentations.js';
|
|
|
2
2
|
import { AdminClientsService } from '../admin_clients_service.js';
|
|
3
3
|
import { clientCreateInput, clientInputValidator, clientPartialInput, } from '../admin_validators.js';
|
|
4
4
|
import { ClientMetadataError } from '../client_metadata.js';
|
|
5
|
+
import { listClientsPage, withListQuery } from '../list_api.js';
|
|
5
6
|
import { apiError, clientDto, createdClientDto } from './dto.js';
|
|
6
7
|
/** Resolve o serviço (== OidcService) + o AdminClientsService. */
|
|
7
8
|
async function clientsService(ctx) {
|
|
@@ -21,13 +22,16 @@ function invalidMetadata(ctx, err) {
|
|
|
21
22
|
* em create/regenerate. Audita create/update/delete.
|
|
22
23
|
*/
|
|
23
24
|
export default class ApiClientsController {
|
|
25
|
+
/**
|
|
26
|
+
* GET /clients — `?search=&sort=clientId|clientName&page=&size=` →
|
|
27
|
+
* `{ data, meta: { total, perPage, currentPage, lastPage, firstPage }, canList }`.
|
|
28
|
+
*/
|
|
24
29
|
async index(ctx) {
|
|
25
30
|
const { svc } = await clientsService(ctx);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
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
|
+
});
|
|
31
35
|
}
|
|
32
36
|
async show(ctx) {
|
|
33
37
|
const { svc } = await clientsService(ctx);
|
|
@@ -5,14 +5,24 @@ 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
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* GET /audit — newest first, `?filter[type]=&filter[subject]=&after=&before=&first=&last=` →
|
|
10
|
+
* `{ data, meta: { nextCursor, prevCursor, hasNext, hasPrev } }` (501 JSON when the sink
|
|
11
|
+
* cannot be queried).
|
|
12
|
+
*/
|
|
9
13
|
audit(ctx: HttpContext): Promise<void | {
|
|
10
|
-
meta:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
meta: import("@adonis-agora/filter").CursorPageMeta;
|
|
15
|
+
data: {
|
|
16
|
+
id: string;
|
|
17
|
+
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";
|
|
18
|
+
accountId: string | null;
|
|
19
|
+
email: string | null;
|
|
20
|
+
clientId: string | null;
|
|
21
|
+
actorId: string | null;
|
|
22
|
+
ip: string | null;
|
|
23
|
+
metadata: Record<string, unknown> | null;
|
|
24
|
+
createdAt: string | null;
|
|
25
|
+
}[];
|
|
16
26
|
}>;
|
|
17
27
|
/** GET /stats — métricas-resumo do IdP (totais + MAU + séries de 30 dias). */
|
|
18
28
|
stats(ctx: HttpContext): Promise<import("../admin_stats_service.js").AdminStats>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
|
-
import { ADMIN_LIST_HTTP_DEFAULT_SIZE, parseListPage, parseListSize } from '../../pagination.js';
|
|
3
2
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
4
3
|
import { computeAdminStats } from '../admin_stats_service.js';
|
|
5
4
|
import { tokenVerifyValidator } from '../admin_validators.js';
|
|
5
|
+
import { listAuditPage, withListQuery } from '../list_api.js';
|
|
6
6
|
import { apiError, auditDto } from './dto.js';
|
|
7
7
|
import { TokenVerifyService } from './token_verify_service.js';
|
|
8
8
|
/**
|
|
@@ -10,7 +10,11 @@ import { TokenVerifyService } from './token_verify_service.js';
|
|
|
10
10
|
* introspecção genérica de token (`POST /tokens/verify`).
|
|
11
11
|
*/
|
|
12
12
|
export default class ApiMiscController {
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* GET /audit — newest first, `?filter[type]=&filter[subject]=&after=&before=&first=&last=` →
|
|
15
|
+
* `{ data, meta: { nextCursor, prevCursor, hasNext, hasPrev } }` (501 JSON when the sink
|
|
16
|
+
* cannot be queried).
|
|
17
|
+
*/
|
|
14
18
|
async audit(ctx) {
|
|
15
19
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
16
20
|
const cfg = service.config;
|
|
@@ -20,12 +24,10 @@ export default class ApiMiscController {
|
|
|
20
24
|
.status(501)
|
|
21
25
|
.send(apiError('not_implemented', 'O sink de auditoria configurado não suporta consulta.'));
|
|
22
26
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const result = await sink.list({ page, size, type, subject });
|
|
28
|
-
return { meta: { page, size, total: result.total }, data: result.data.map(auditDto) };
|
|
27
|
+
return withListQuery(ctx, async () => {
|
|
28
|
+
const page = await listAuditPage(sink, ctx.request.qs());
|
|
29
|
+
return { ...page, data: page.data.map(auditDto) };
|
|
30
|
+
});
|
|
29
31
|
}
|
|
30
32
|
/** GET /stats — métricas-resumo do IdP (totais + MAU + séries de 30 dias). */
|
|
31
33
|
async stats(ctx) {
|
|
@@ -5,8 +5,12 @@ import type { HttpContext } from '@adonisjs/core/http';
|
|
|
5
5
|
* Todas as rotas ficam sob `/api/authkit/v1/organizations`.
|
|
6
6
|
*/
|
|
7
7
|
export default class ApiOrgsController {
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* GET /organizations — `?search=&sort=name|slug|createdAt|memberCount&page=&size=` →
|
|
10
|
+
* `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`, with member counts.
|
|
11
|
+
*/
|
|
9
12
|
index(ctx: HttpContext): Promise<void | {
|
|
13
|
+
meta: import("@adonis-agora/filter").OffsetPageMeta;
|
|
10
14
|
data: {
|
|
11
15
|
id: string;
|
|
12
16
|
name: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import { orgAddMemberValidator, orgCreateValidator, orgInvitationValidator, orgMemberRoleValidator, orgUpdateValidator, } from '../admin_validators.js';
|
|
3
|
+
import { listOrgsPage, withListQuery } from '../list_api.js';
|
|
3
4
|
import { authkitOrigin } from '../origin.js';
|
|
4
5
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
5
6
|
import { AdminOrgsService } from './admin_orgs_service.js';
|
|
@@ -20,19 +21,20 @@ function notSupported(ctx) {
|
|
|
20
21
|
* Todas as rotas ficam sob `/api/authkit/v1/organizations`.
|
|
21
22
|
*/
|
|
22
23
|
export default class ApiOrgsController {
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* GET /organizations — `?search=&sort=name|slug|createdAt|memberCount&page=&size=` →
|
|
26
|
+
* `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`, with member counts.
|
|
27
|
+
*/
|
|
24
28
|
async index(ctx) {
|
|
25
29
|
const { cfg, service } = await ctxBits(ctx);
|
|
26
30
|
const svc = new AdminOrgsService(cfg, service);
|
|
27
31
|
const result = await svc.listOrgs();
|
|
28
|
-
if (!Array.isArray(result))
|
|
29
|
-
if (result.reason === 'not_supported')
|
|
30
|
-
return notSupported(ctx);
|
|
31
|
-
}
|
|
32
|
-
const orgs = result;
|
|
33
|
-
if (!Array.isArray(orgs))
|
|
32
|
+
if (!Array.isArray(result))
|
|
34
33
|
return notSupported(ctx);
|
|
35
|
-
return
|
|
34
|
+
return withListQuery(ctx, async () => {
|
|
35
|
+
const page = await listOrgsPage(result, ctx.request.qs());
|
|
36
|
+
return { ...page, data: page.data.map(orgDto) };
|
|
37
|
+
});
|
|
36
38
|
}
|
|
37
39
|
/** POST /organizations — cria uma org. Body: { name, slug, ownerAccountId, logoUrl? } */
|
|
38
40
|
async store(ctx) {
|
|
@@ -6,14 +6,21 @@ import type { HttpContext } from '@adonisjs/core/http';
|
|
|
6
6
|
*/
|
|
7
7
|
export default class ApiUsersController {
|
|
8
8
|
#private;
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
/**
|
|
10
|
+
* GET /users — `?search=&sort=email&page=&size=` →
|
|
11
|
+
* `{ data, meta: { total, perPage, currentPage, lastPage, firstPage } }`.
|
|
12
|
+
*/
|
|
13
|
+
index(ctx: HttpContext): Promise<void | {
|
|
14
|
+
meta: import("@adonis-agora/filter").OffsetPageMeta;
|
|
15
|
+
data: {
|
|
16
|
+
id: string;
|
|
17
|
+
email: string;
|
|
18
|
+
name: string | null;
|
|
19
|
+
avatarUrl: string | null;
|
|
20
|
+
globalRoles: string[];
|
|
21
|
+
disabled: boolean;
|
|
22
|
+
roles: string[];
|
|
23
|
+
}[];
|
|
17
24
|
}>;
|
|
18
25
|
/** GET /users/:id */
|
|
19
26
|
show(ctx: HttpContext): Promise<void | {
|