@dudousxd/adonis-authkit-server 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +14 -2
- package/build/index.js +9 -1
- package/build/src/define_config.d.ts +25 -0
- package/build/src/define_config.js +7 -0
- package/build/src/host/admin_api/admin_api_guard.d.ts +8 -0
- package/build/src/host/admin_api/admin_api_guard.js +41 -0
- package/build/src/host/admin_api/admin_users_service.d.ts +59 -0
- package/build/src/host/admin_api/admin_users_service.js +112 -0
- package/build/src/host/admin_api/api_clients_controller.d.ts +48 -0
- package/build/src/host/admin_api/api_clients_controller.js +104 -0
- package/build/src/host/admin_api/api_misc_controller.d.ts +17 -0
- package/build/src/host/admin_api/api_misc_controller.js +37 -0
- package/build/src/host/admin_api/api_users_controller.d.ts +78 -0
- package/build/src/host/admin_api/api_users_controller.js +135 -0
- package/build/src/host/admin_api/dto.d.ts +57 -0
- package/build/src/host/admin_api/dto.js +62 -0
- package/build/src/host/admin_api/token_verify_service.d.ts +34 -0
- package/build/src/host/admin_api/token_verify_service.js +74 -0
- package/build/src/host/controllers/admin/admin_users_controller.js +7 -58
- package/build/src/host/register_auth_host.d.ts +7 -0
- package/build/src/host/register_auth_host.js +39 -0
- package/package.json +1 -1
package/build/index.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export { withCredentials } from './src/mixins/with_credentials.js';
|
|
|
5
5
|
export { withMfa } from './src/mixins/with_mfa.js';
|
|
6
6
|
export { OidcService } from './src/provider/oidc_service.js';
|
|
7
7
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
8
|
-
export type { AuthServerConfigInput, ResolvedServerConfig, DynamicRegistrationConfigInput, ResolvedDynamicRegistrationConfig, AdminConfigInput, ResolvedAdminConfig, } from './src/define_config.js';
|
|
9
|
-
export { resolveAdmin, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
|
|
8
|
+
export type { AuthServerConfigInput, ResolvedServerConfig, DynamicRegistrationConfigInput, ResolvedDynamicRegistrationConfig, AdminConfigInput, ResolvedAdminConfig, AdminApiConfigInput, ResolvedAdminApiConfig, } from './src/define_config.js';
|
|
9
|
+
export { resolveAdmin, resolveAdminApi, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
|
|
10
10
|
export type { WebauthnConfigInput, ResolvedWebauthnConfig } from './src/define_config.js';
|
|
11
11
|
export { resolvePasswordless } from './src/define_config.js';
|
|
12
12
|
export type { PasswordlessConfigInput, ResolvedPasswordlessConfig, } from './src/define_config.js';
|
|
@@ -42,6 +42,18 @@ export type { NotificationsConfigInput, ResolvedNotificationsConfig, } from './s
|
|
|
42
42
|
export type { RateLimitConfigInput, RateLimitBucket, ResolvedRateLimitConfig, } from './src/define_config.js';
|
|
43
43
|
export { createAuthThrottles } from './src/host/rate_limit.js';
|
|
44
44
|
export type { AuthThrottles, ThrottleMiddleware } from './src/host/rate_limit.js';
|
|
45
|
+
/**
|
|
46
|
+
* Admin services compartilhados pelo console (B6/HTML), pela Admin REST API (R6)
|
|
47
|
+
* e pelo driver `embedded` do @dudousxd/adonis-authkit-sdk (in-process).
|
|
48
|
+
*/
|
|
49
|
+
export { AdminUsersService } from './src/host/admin_api/admin_users_service.js';
|
|
50
|
+
export type { AdminActor, CreateUserInput as AdminCreateUserInput, CreateUserResult as AdminCreateUserResult, } from './src/host/admin_api/admin_users_service.js';
|
|
51
|
+
export { AdminClientsService } from './src/host/admin_clients_service.js';
|
|
52
|
+
export type { AdminClient, ClientInput as AdminClientInput, CreatedClient, TokenEndpointAuthMethod, } from './src/host/admin_clients_service.js';
|
|
53
|
+
export { AdminSessionsService } from './src/host/admin_sessions_service.js';
|
|
54
|
+
export type { AdminSession, AdminGrant, RevokeResult, } from './src/host/admin_sessions_service.js';
|
|
55
|
+
export { TokenVerifyService } from './src/host/admin_api/token_verify_service.js';
|
|
56
|
+
export type { VerifyResult } from './src/host/admin_api/token_verify_service.js';
|
|
45
57
|
/**
|
|
46
58
|
* Configure hook + stubsRoot resolvidos pelo `node ace configure @dudousxd/adonis-authkit-server`.
|
|
47
59
|
* O comando do AdonisJS importa o entrypoint principal e procura por estes exports.
|
package/build/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { withCredentials } from './src/mixins/with_credentials.js';
|
|
|
5
5
|
export { withMfa } from './src/mixins/with_mfa.js';
|
|
6
6
|
export { OidcService } from './src/provider/oidc_service.js';
|
|
7
7
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
8
|
-
export { resolveAdmin, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
|
|
8
|
+
export { resolveAdmin, resolveAdminApi, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
|
|
9
9
|
export { resolvePasswordless } from './src/define_config.js';
|
|
10
10
|
export { resolveTrustedDevices, isTrustedDeviceValid, buildTrustedDevicePayload, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
|
|
11
11
|
export { lucidAccountStore } from './src/accounts/lucid_account_store.js';
|
|
@@ -24,6 +24,14 @@ export { resolveMessages, translate, DEFAULT_MESSAGES, PT_BR_MESSAGES, BUILTIN_M
|
|
|
24
24
|
export { registerAuthHost } from './src/host/register_auth_host.js';
|
|
25
25
|
export { resolveRateLimit, resolveNotifications } from './src/define_config.js';
|
|
26
26
|
export { createAuthThrottles } from './src/host/rate_limit.js';
|
|
27
|
+
/**
|
|
28
|
+
* Admin services compartilhados pelo console (B6/HTML), pela Admin REST API (R6)
|
|
29
|
+
* e pelo driver `embedded` do @dudousxd/adonis-authkit-sdk (in-process).
|
|
30
|
+
*/
|
|
31
|
+
export { AdminUsersService } from './src/host/admin_api/admin_users_service.js';
|
|
32
|
+
export { AdminClientsService } from './src/host/admin_clients_service.js';
|
|
33
|
+
export { AdminSessionsService } from './src/host/admin_sessions_service.js';
|
|
34
|
+
export { TokenVerifyService } from './src/host/admin_api/token_verify_service.js';
|
|
27
35
|
/**
|
|
28
36
|
* Configure hook + stubsRoot resolvidos pelo `node ace configure @dudousxd/adonis-authkit-server`.
|
|
29
37
|
* O comando do AdonisJS importa o entrypoint principal e procura por estes exports.
|
|
@@ -278,6 +278,23 @@ export interface ResolvedAdminConfig {
|
|
|
278
278
|
roles: string[];
|
|
279
279
|
}
|
|
280
280
|
export declare function resolveAdmin(input?: AdminConfigInput): ResolvedAdminConfig;
|
|
281
|
+
/**
|
|
282
|
+
* Admin REST API (R6) — superfície de gestão machine-to-machine, consumida por um
|
|
283
|
+
* futuro SDK. Default: DESLIGADA. A autenticação é por API key (Bearer), checada
|
|
284
|
+
* em tempo constante contra `apiKeys`. Independente do console admin (B6): pode
|
|
285
|
+
* ligar uma sem a outra.
|
|
286
|
+
*/
|
|
287
|
+
export interface AdminApiConfigInput {
|
|
288
|
+
/** Liga a Admin REST API (`/api/authkit/v1`). Default: false. */
|
|
289
|
+
enabled: boolean;
|
|
290
|
+
/** API keys aceitas no header `Authorization: Bearer <key>`. */
|
|
291
|
+
apiKeys?: string[];
|
|
292
|
+
}
|
|
293
|
+
export interface ResolvedAdminApiConfig {
|
|
294
|
+
enabled: boolean;
|
|
295
|
+
apiKeys: string[];
|
|
296
|
+
}
|
|
297
|
+
export declare function resolveAdminApi(input?: AdminApiConfigInput): ResolvedAdminApiConfig;
|
|
281
298
|
/**
|
|
282
299
|
* Parâmetros do Relying Party (RP) das cerimônias WebAuthn / passkeys. Quando
|
|
283
300
|
* omitidos, são derivados do `issuer`: `rpId` = hostname (sem porta), `origin` =
|
|
@@ -387,6 +404,12 @@ export interface AuthServerConfigInput {
|
|
|
387
404
|
* (a montagem das rotas acontece antes do config resolver).
|
|
388
405
|
*/
|
|
389
406
|
admin?: AdminConfigInput;
|
|
407
|
+
/**
|
|
408
|
+
* Admin REST API (R6). Default: desligada. Quando ligada, o host também deve
|
|
409
|
+
* passar `adminApi: true` em {@link AuthHostOptions} no registro de rotas (a
|
|
410
|
+
* montagem das rotas acontece antes do config resolver). Autenticação por API key.
|
|
411
|
+
*/
|
|
412
|
+
adminApi?: AdminApiConfigInput;
|
|
390
413
|
}
|
|
391
414
|
export interface ResolvedServerConfig {
|
|
392
415
|
issuer: string;
|
|
@@ -442,6 +465,8 @@ export interface ResolvedServerConfig {
|
|
|
442
465
|
passwordless: ResolvedPasswordlessConfig;
|
|
443
466
|
/** Console admin resolvido (sempre presente; default desligado). */
|
|
444
467
|
admin: ResolvedAdminConfig;
|
|
468
|
+
/** Admin REST API resolvida (sempre presente; default desligada). */
|
|
469
|
+
adminApi: ResolvedAdminApiConfig;
|
|
445
470
|
/** Catálogo de mensagens ativo (locale resolvido), pronto para os renderers. */
|
|
446
471
|
messages: AuthMessages;
|
|
447
472
|
/** Locale ativo (default 'pt-BR'). */
|
|
@@ -92,6 +92,12 @@ export function resolveAdmin(input) {
|
|
|
92
92
|
roles: input?.roles && input.roles.length > 0 ? input.roles : ['ADMIN'],
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
+
export function resolveAdminApi(input) {
|
|
96
|
+
return {
|
|
97
|
+
enabled: input?.enabled ?? false,
|
|
98
|
+
apiKeys: input?.apiKeys ?? [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
95
101
|
/**
|
|
96
102
|
* Resolve os parâmetros do RP de WebAuthn a partir do `issuer` quando omitidos.
|
|
97
103
|
* `rpName` cai no `fallbackName` (branding/mfaIssuer) quando ausente.
|
|
@@ -189,6 +195,7 @@ export function defineConfig(config) {
|
|
|
189
195
|
trustedDevices: resolveTrustedDevices(config.trustedDevices),
|
|
190
196
|
passwordless: resolvePasswordless(config.passwordless),
|
|
191
197
|
admin: resolveAdmin(config.admin),
|
|
198
|
+
adminApi: resolveAdminApi(config.adminApi),
|
|
192
199
|
messages: resolveMessages(config.i18n),
|
|
193
200
|
locale: config.i18n?.locale ?? 'pt-BR',
|
|
194
201
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard da Admin REST API (R6). Espelha o `adminGuard` do console (B6):
|
|
3
|
+
* 0. `config.adminApi.enabled` desligado → 404 (não vaza a existência da API);
|
|
4
|
+
* 1. `Authorization: Bearer <key>` ausente/inválido → 401 JSON.
|
|
5
|
+
* Sem nenhuma API key configurada, qualquer request é 401 (fail-safe). As respostas
|
|
6
|
+
* de erro seguem o envelope `{ error: { code, message } }`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const adminApiGuard: (ctx: any, next: () => Promise<void>) => Promise<any>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Compara o Bearer recebido contra a lista de API keys em tempo constante. Cada
|
|
4
|
+
* comparação só roda quando os comprimentos batem (o `timingSafeEqual` lança em
|
|
5
|
+
* tamanhos diferentes); o curto-circuito por comprimento NÃO vaza a key (só o seu
|
|
6
|
+
* tamanho), aceitável para um segredo de alta entropia gerado pelo operador.
|
|
7
|
+
*/
|
|
8
|
+
function keyMatches(header, keys) {
|
|
9
|
+
if (!header || !header.startsWith('Bearer '))
|
|
10
|
+
return false;
|
|
11
|
+
const provided = Buffer.from(header.slice(7));
|
|
12
|
+
let matched = false;
|
|
13
|
+
for (const key of keys) {
|
|
14
|
+
const expected = Buffer.from(key);
|
|
15
|
+
if (provided.length === expected.length && timingSafeEqual(provided, expected)) {
|
|
16
|
+
matched = true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return matched;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Guard da Admin REST API (R6). Espelha o `adminGuard` do console (B6):
|
|
23
|
+
* 0. `config.adminApi.enabled` desligado → 404 (não vaza a existência da API);
|
|
24
|
+
* 1. `Authorization: Bearer <key>` ausente/inválido → 401 JSON.
|
|
25
|
+
* Sem nenhuma API key configurada, qualquer request é 401 (fail-safe). As respostas
|
|
26
|
+
* de erro seguem o envelope `{ error: { code, message } }`.
|
|
27
|
+
*/
|
|
28
|
+
export const adminApiGuard = async (ctx, next) => {
|
|
29
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
30
|
+
const cfg = service.config;
|
|
31
|
+
if (!cfg.adminApi.enabled) {
|
|
32
|
+
return ctx.response.notFound();
|
|
33
|
+
}
|
|
34
|
+
const keys = cfg.adminApi.apiKeys;
|
|
35
|
+
if (keys.length === 0 || !keyMatches(ctx.request.header('authorization'), keys)) {
|
|
36
|
+
return ctx.response.unauthorized({
|
|
37
|
+
error: { code: 'unauthorized', message: 'API key ausente ou inválida.' },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return next();
|
|
41
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
import type { ResolvedServerConfig } from '../../define_config.js';
|
|
3
|
+
import type { AuthAccount } from '../../accounts/account_store.js';
|
|
4
|
+
/** Quem disparou a operação (para auditoria). `admin-api` quando via REST API. */
|
|
5
|
+
export interface AdminActor {
|
|
6
|
+
actorId: string | null;
|
|
7
|
+
ip: string | null;
|
|
8
|
+
/** Marca metadata da auditoria — 'admin-api' nas escritas via REST. */
|
|
9
|
+
source?: 'admin-api';
|
|
10
|
+
}
|
|
11
|
+
export interface CreateUserInput {
|
|
12
|
+
email: string;
|
|
13
|
+
name?: string | null;
|
|
14
|
+
password?: string | null;
|
|
15
|
+
/** Quando true (e sem password), cria com senha aleatória e envia convite/reset. */
|
|
16
|
+
invite?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export type CreateUserResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
account: AuthAccount;
|
|
21
|
+
invited: boolean;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
reason: 'email_taken';
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Lógica de gestão de usuários compartilhada entre o console admin (B6, HTML) e a
|
|
28
|
+
* Admin REST API (R6, JSON). Encapsula o fluxo "create + invite", reset de senha,
|
|
29
|
+
* troca de status e atualização de perfil/roles — todos auditando com o `actor`
|
|
30
|
+
* informado (`admin-api` nas chamadas REST).
|
|
31
|
+
*/
|
|
32
|
+
export declare class AdminUsersService {
|
|
33
|
+
private cfg;
|
|
34
|
+
constructor(cfg: ResolvedServerConfig);
|
|
35
|
+
/**
|
|
36
|
+
* Cria uma conta. Com `password`: já nasce com a senha. Sem `password` (ou
|
|
37
|
+
* `invite: true`): cria com senha aleatória forte e dispara o e-mail de reset
|
|
38
|
+
* (o usuário define a própria). Audita `user.created`.
|
|
39
|
+
*/
|
|
40
|
+
create(ctx: HttpContext, input: CreateUserInput, actor: AdminActor): Promise<CreateUserResult>;
|
|
41
|
+
/** Emite token de reset + envia e-mail. Retorna a conta (ou null se inexistente). */
|
|
42
|
+
resetPassword(ctx: HttpContext, accountId: string, actor: AdminActor): Promise<AuthAccount | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Habilita/desabilita uma conta. Retorna false quando o store não suporta a
|
|
45
|
+
* capacidade (o caller responde 409). Audita `user.disabled`/`user.enabled`.
|
|
46
|
+
*/
|
|
47
|
+
setStatus(accountId: string, disable: boolean, actor: AdminActor): Promise<boolean>;
|
|
48
|
+
/** Substitui as roles globais de uma conta (normaliza nada — recebe array pronto). */
|
|
49
|
+
setGlobalRoles(accountId: string, roles: string[]): Promise<void>;
|
|
50
|
+
/** Atualiza nome/avatar (capacidade opcional). Retorna a conta ou null. */
|
|
51
|
+
updateProfile(accountId: string, patch: {
|
|
52
|
+
name?: string | null;
|
|
53
|
+
avatarUrl?: string | null;
|
|
54
|
+
}): Promise<AuthAccount | null>;
|
|
55
|
+
/** Indica se a conta está desabilitada (false quando a capacidade não existe). */
|
|
56
|
+
isDisabled(accountId: string): Promise<boolean>;
|
|
57
|
+
/** Emite o token de reset e dispara o e-mail (hook do config tem prioridade). */
|
|
58
|
+
private sendResetEmail;
|
|
59
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { supportsAccountStatus, supportsProfile } from '../../accounts/account_store.js';
|
|
3
|
+
import { sendPasswordResetEmail } from '../default_mailer.js';
|
|
4
|
+
/**
|
|
5
|
+
* Lógica de gestão de usuários compartilhada entre o console admin (B6, HTML) e a
|
|
6
|
+
* Admin REST API (R6, JSON). Encapsula o fluxo "create + invite", reset de senha,
|
|
7
|
+
* troca de status e atualização de perfil/roles — todos auditando com o `actor`
|
|
8
|
+
* informado (`admin-api` nas chamadas REST).
|
|
9
|
+
*/
|
|
10
|
+
export class AdminUsersService {
|
|
11
|
+
cfg;
|
|
12
|
+
constructor(cfg) {
|
|
13
|
+
this.cfg = cfg;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Cria uma conta. Com `password`: já nasce com a senha. Sem `password` (ou
|
|
17
|
+
* `invite: true`): cria com senha aleatória forte e dispara o e-mail de reset
|
|
18
|
+
* (o usuário define a própria). Audita `user.created`.
|
|
19
|
+
*/
|
|
20
|
+
async create(ctx, input, actor) {
|
|
21
|
+
const store = this.cfg.accountStore;
|
|
22
|
+
const existing = await store.findByEmail(input.email);
|
|
23
|
+
if (existing)
|
|
24
|
+
return { ok: false, reason: 'email_taken' };
|
|
25
|
+
const hasPassword = !!input.password;
|
|
26
|
+
const initialPassword = input.password ?? randomBytes(24).toString('hex');
|
|
27
|
+
const account = await store.create({
|
|
28
|
+
email: input.email,
|
|
29
|
+
password: initialPassword,
|
|
30
|
+
fullName: input.name ?? null,
|
|
31
|
+
});
|
|
32
|
+
await this.cfg.audit?.record({
|
|
33
|
+
type: 'user.created',
|
|
34
|
+
accountId: account.id,
|
|
35
|
+
email: input.email,
|
|
36
|
+
actorId: actor.actorId,
|
|
37
|
+
ip: actor.ip,
|
|
38
|
+
metadata: { invited: !hasPassword, ...(actor.source ? { actor: actor.source } : {}) },
|
|
39
|
+
});
|
|
40
|
+
if (!hasPassword) {
|
|
41
|
+
await this.sendResetEmail(ctx, account.email);
|
|
42
|
+
}
|
|
43
|
+
return { ok: true, account, invited: !hasPassword };
|
|
44
|
+
}
|
|
45
|
+
/** Emite token de reset + envia e-mail. Retorna a conta (ou null se inexistente). */
|
|
46
|
+
async resetPassword(ctx, accountId, actor) {
|
|
47
|
+
const account = await this.cfg.accountStore.findById(accountId);
|
|
48
|
+
if (!account)
|
|
49
|
+
return null;
|
|
50
|
+
await this.sendResetEmail(ctx, account.email);
|
|
51
|
+
await this.cfg.audit?.record({
|
|
52
|
+
type: 'user.password_reset_sent',
|
|
53
|
+
accountId,
|
|
54
|
+
email: account.email,
|
|
55
|
+
actorId: actor.actorId,
|
|
56
|
+
ip: actor.ip,
|
|
57
|
+
...(actor.source ? { metadata: { actor: actor.source } } : {}),
|
|
58
|
+
});
|
|
59
|
+
return account;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Habilita/desabilita uma conta. Retorna false quando o store não suporta a
|
|
63
|
+
* capacidade (o caller responde 409). Audita `user.disabled`/`user.enabled`.
|
|
64
|
+
*/
|
|
65
|
+
async setStatus(accountId, disable, actor) {
|
|
66
|
+
const store = this.cfg.accountStore;
|
|
67
|
+
if (!supportsAccountStatus(store))
|
|
68
|
+
return false;
|
|
69
|
+
if (disable)
|
|
70
|
+
await store.disableAccount(accountId);
|
|
71
|
+
else
|
|
72
|
+
await store.enableAccount(accountId);
|
|
73
|
+
await this.cfg.audit?.record({
|
|
74
|
+
type: disable ? 'user.disabled' : 'user.enabled',
|
|
75
|
+
accountId,
|
|
76
|
+
actorId: actor.actorId,
|
|
77
|
+
ip: actor.ip,
|
|
78
|
+
...(actor.source ? { metadata: { actor: actor.source } } : {}),
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** Substitui as roles globais de uma conta (normaliza nada — recebe array pronto). */
|
|
83
|
+
async setGlobalRoles(accountId, roles) {
|
|
84
|
+
await this.cfg.accountStore.setGlobalRoles(accountId, roles);
|
|
85
|
+
}
|
|
86
|
+
/** Atualiza nome/avatar (capacidade opcional). Retorna a conta ou null. */
|
|
87
|
+
async updateProfile(accountId, patch) {
|
|
88
|
+
const store = this.cfg.accountStore;
|
|
89
|
+
if (!supportsProfile(store))
|
|
90
|
+
return null;
|
|
91
|
+
return store.updateProfile(accountId, patch);
|
|
92
|
+
}
|
|
93
|
+
/** Indica se a conta está desabilitada (false quando a capacidade não existe). */
|
|
94
|
+
async isDisabled(accountId) {
|
|
95
|
+
const store = this.cfg.accountStore;
|
|
96
|
+
return supportsAccountStatus(store) ? store.isDisabled(accountId) : false;
|
|
97
|
+
}
|
|
98
|
+
/** Emite o token de reset e dispara o e-mail (hook do config tem prioridade). */
|
|
99
|
+
async sendResetEmail(ctx, email) {
|
|
100
|
+
const issued = await this.cfg.accountStore.issuePasswordResetToken(email);
|
|
101
|
+
if (!issued)
|
|
102
|
+
return;
|
|
103
|
+
const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
|
|
104
|
+
const resetUrl = `${origin}/auth/reset-password?token=${encodeURIComponent(issued.token)}`;
|
|
105
|
+
if (this.cfg.mail?.onPasswordReset) {
|
|
106
|
+
await this.cfg.mail.onPasswordReset({ email, resetUrl, token: issued.token });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await sendPasswordResetEmail(ctx, { email, resetUrl });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Recurso de clients OIDC da Admin REST API (R6). Reaproveita o
|
|
5
|
+
* {@link AdminClientsService} (mesmo que o console B6). O secret é retornado UMA vez
|
|
6
|
+
* em create/regenerate. Audita create/update/delete.
|
|
7
|
+
*/
|
|
8
|
+
export default class ApiClientsController {
|
|
9
|
+
index(ctx: HttpContext): Promise<{
|
|
10
|
+
data: {
|
|
11
|
+
clientId: string;
|
|
12
|
+
confidential: boolean;
|
|
13
|
+
grants: string[];
|
|
14
|
+
redirectUris: string[];
|
|
15
|
+
postLogoutRedirectUris: string[];
|
|
16
|
+
tokenEndpointAuthMethod: string;
|
|
17
|
+
}[];
|
|
18
|
+
canList: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
show(ctx: HttpContext): Promise<void | {
|
|
21
|
+
clientId: string;
|
|
22
|
+
confidential: boolean;
|
|
23
|
+
grants: string[];
|
|
24
|
+
redirectUris: string[];
|
|
25
|
+
postLogoutRedirectUris: string[];
|
|
26
|
+
tokenEndpointAuthMethod: string;
|
|
27
|
+
}>;
|
|
28
|
+
store(ctx: HttpContext): Promise<{
|
|
29
|
+
clientId: string;
|
|
30
|
+
clientSecret: string | null;
|
|
31
|
+
}>;
|
|
32
|
+
update(ctx: HttpContext): Promise<void | {
|
|
33
|
+
clientId: string;
|
|
34
|
+
confidential: boolean;
|
|
35
|
+
grants: string[];
|
|
36
|
+
redirectUris: string[];
|
|
37
|
+
postLogoutRedirectUris: string[];
|
|
38
|
+
tokenEndpointAuthMethod: string;
|
|
39
|
+
}>;
|
|
40
|
+
regenerateSecret(ctx: HttpContext): Promise<void | {
|
|
41
|
+
clientId: any;
|
|
42
|
+
clientSecret: string;
|
|
43
|
+
}>;
|
|
44
|
+
destroy(ctx: HttpContext): Promise<{
|
|
45
|
+
clientId: any;
|
|
46
|
+
deleted: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { AdminClientsService } from '../admin_clients_service.js';
|
|
3
|
+
import { clientDto, createdClientDto, apiError } from './dto.js';
|
|
4
|
+
/** Resolve o serviço (== OidcService) + o AdminClientsService. */
|
|
5
|
+
async function clientsService(ctx) {
|
|
6
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
7
|
+
return { service, svc: new AdminClientsService(service) };
|
|
8
|
+
}
|
|
9
|
+
function asArray(value) {
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.map((v) => String(v)).filter(Boolean);
|
|
12
|
+
if (typeof value === 'string' && value.trim())
|
|
13
|
+
return [value.trim()];
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
function readInput(ctx) {
|
|
17
|
+
return {
|
|
18
|
+
clientId: ctx.request.input('clientId')?.trim() || undefined,
|
|
19
|
+
redirectUris: asArray(ctx.request.input('redirectUris')),
|
|
20
|
+
postLogoutRedirectUris: asArray(ctx.request.input('postLogoutRedirectUris')),
|
|
21
|
+
grantTypes: asArray(ctx.request.input('grantTypes')),
|
|
22
|
+
tokenEndpointAuthMethod: ctx.request.input('tokenEndpointAuthMethod', 'client_secret_basic'),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Recurso de clients OIDC da Admin REST API (R6). Reaproveita o
|
|
27
|
+
* {@link AdminClientsService} (mesmo que o console B6). O secret é retornado UMA vez
|
|
28
|
+
* em create/regenerate. Audita create/update/delete.
|
|
29
|
+
*/
|
|
30
|
+
export default class ApiClientsController {
|
|
31
|
+
async index(ctx) {
|
|
32
|
+
const { svc } = await clientsService(ctx);
|
|
33
|
+
if (!svc.canList) {
|
|
34
|
+
return { data: [], canList: false };
|
|
35
|
+
}
|
|
36
|
+
const clients = await svc.list();
|
|
37
|
+
return { data: clients.map(clientDto), canList: true };
|
|
38
|
+
}
|
|
39
|
+
async show(ctx) {
|
|
40
|
+
const { svc } = await clientsService(ctx);
|
|
41
|
+
const client = await svc.find(ctx.request.param('id'));
|
|
42
|
+
if (!client)
|
|
43
|
+
return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
|
|
44
|
+
return clientDto(client);
|
|
45
|
+
}
|
|
46
|
+
async store(ctx) {
|
|
47
|
+
const { service, svc } = await clientsService(ctx);
|
|
48
|
+
const input = readInput(ctx);
|
|
49
|
+
const created = await svc.create(input);
|
|
50
|
+
await service.config.audit?.record({
|
|
51
|
+
type: 'client.created',
|
|
52
|
+
clientId: created.clientId,
|
|
53
|
+
ip: ctx.request.ip?.() ?? null,
|
|
54
|
+
metadata: { actor: 'admin-api' },
|
|
55
|
+
});
|
|
56
|
+
ctx.response.status(201);
|
|
57
|
+
return createdClientDto(created);
|
|
58
|
+
}
|
|
59
|
+
async update(ctx) {
|
|
60
|
+
const { service, svc } = await clientsService(ctx);
|
|
61
|
+
const id = ctx.request.param('id');
|
|
62
|
+
const existing = await svc.find(id);
|
|
63
|
+
if (!existing)
|
|
64
|
+
return ctx.response.notFound(apiError('not_found', 'Client não encontrado.'));
|
|
65
|
+
await svc.update(id, readInput(ctx));
|
|
66
|
+
await service.config.audit?.record({
|
|
67
|
+
type: 'client.updated',
|
|
68
|
+
clientId: id,
|
|
69
|
+
ip: ctx.request.ip?.() ?? null,
|
|
70
|
+
metadata: { actor: 'admin-api' },
|
|
71
|
+
});
|
|
72
|
+
const updated = await svc.find(id);
|
|
73
|
+
return clientDto(updated);
|
|
74
|
+
}
|
|
75
|
+
async regenerateSecret(ctx) {
|
|
76
|
+
const { service, svc } = await clientsService(ctx);
|
|
77
|
+
const id = ctx.request.param('id');
|
|
78
|
+
try {
|
|
79
|
+
const secret = await svc.regenerateSecret(id);
|
|
80
|
+
await service.config.audit?.record({
|
|
81
|
+
type: 'client.updated',
|
|
82
|
+
clientId: id,
|
|
83
|
+
ip: ctx.request.ip?.() ?? null,
|
|
84
|
+
metadata: { actor: 'admin-api', action: 'regenerate_secret' },
|
|
85
|
+
});
|
|
86
|
+
return { clientId: id, clientSecret: secret };
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
return ctx.response.conflict(apiError('cannot_regenerate', e.message));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async destroy(ctx) {
|
|
93
|
+
const { service, svc } = await clientsService(ctx);
|
|
94
|
+
const id = ctx.request.param('id');
|
|
95
|
+
await svc.delete(id);
|
|
96
|
+
await service.config.audit?.record({
|
|
97
|
+
type: 'client.deleted',
|
|
98
|
+
clientId: id,
|
|
99
|
+
ip: ctx.request.ip?.() ?? null,
|
|
100
|
+
metadata: { actor: 'admin-api' },
|
|
101
|
+
});
|
|
102
|
+
return { clientId: id, deleted: true };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Endpoints utilitários da Admin REST API: log de auditoria (`GET /audit`) e
|
|
5
|
+
* introspecção genérica de token (`POST /tokens/verify`).
|
|
6
|
+
*/
|
|
7
|
+
export default class ApiMiscController {
|
|
8
|
+
/** GET /audit — listagem paginada (501 JSON quando o sink não consulta). */
|
|
9
|
+
audit(ctx: HttpContext): Promise<void | {
|
|
10
|
+
data: any;
|
|
11
|
+
total: any;
|
|
12
|
+
page: number;
|
|
13
|
+
limit: number;
|
|
14
|
+
}>;
|
|
15
|
+
/** POST /tokens/verify — { token } → resultado de introspecção (PAT ou opaque AT). */
|
|
16
|
+
verify(ctx: HttpContext): Promise<void | import("./token_verify_service.js").VerifyResult>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { TokenVerifyService } from './token_verify_service.js';
|
|
3
|
+
import { auditDto, apiError } from './dto.js';
|
|
4
|
+
/**
|
|
5
|
+
* Endpoints utilitários da Admin REST API: log de auditoria (`GET /audit`) e
|
|
6
|
+
* introspecção genérica de token (`POST /tokens/verify`).
|
|
7
|
+
*/
|
|
8
|
+
export default class ApiMiscController {
|
|
9
|
+
/** GET /audit — listagem paginada (501 JSON quando o sink não consulta). */
|
|
10
|
+
async audit(ctx) {
|
|
11
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
12
|
+
const cfg = service.config;
|
|
13
|
+
const sink = cfg.audit;
|
|
14
|
+
if (!sink || typeof sink.list !== 'function') {
|
|
15
|
+
return ctx.response
|
|
16
|
+
.status(501)
|
|
17
|
+
.send(apiError('not_implemented', 'O sink de auditoria configurado não suporta consulta.'));
|
|
18
|
+
}
|
|
19
|
+
const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
|
|
20
|
+
const limit = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('limit', '20'), 10) || 20));
|
|
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, limit, type, subject });
|
|
24
|
+
return { data: result.data.map(auditDto), total: result.total, page, limit };
|
|
25
|
+
}
|
|
26
|
+
/** POST /tokens/verify — { token } → resultado de introspecção (PAT ou opaque AT). */
|
|
27
|
+
async verify(ctx) {
|
|
28
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
29
|
+
const cfg = service.config;
|
|
30
|
+
const token = ctx.request.input('token');
|
|
31
|
+
if (!token || typeof token !== 'string') {
|
|
32
|
+
return ctx.response.badRequest(apiError('invalid_request', 'O campo token é obrigatório.'));
|
|
33
|
+
}
|
|
34
|
+
const verifier = new TokenVerifyService(cfg, service.provider);
|
|
35
|
+
return verifier.verify(token);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
|
+
/**
|
|
4
|
+
* Recurso de usuários da Admin REST API (R6). JSON puro (camelCase), erros no
|
|
5
|
+
* envelope `{ error: { code, message } }`. Toda escrita audita com `actor: 'admin-api'`.
|
|
6
|
+
*/
|
|
7
|
+
export default class ApiUsersController {
|
|
8
|
+
#private;
|
|
9
|
+
/** GET /users — listagem paginada com busca por e-mail. */
|
|
10
|
+
index(ctx: HttpContext): Promise<{
|
|
11
|
+
data: any[];
|
|
12
|
+
total: any;
|
|
13
|
+
page: number;
|
|
14
|
+
limit: number;
|
|
15
|
+
}>;
|
|
16
|
+
/** GET /users/:id */
|
|
17
|
+
show(ctx: HttpContext): Promise<void | {
|
|
18
|
+
id: string;
|
|
19
|
+
email: string;
|
|
20
|
+
name: string | null;
|
|
21
|
+
avatarUrl: string | null;
|
|
22
|
+
globalRoles: string[];
|
|
23
|
+
disabled: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
/** POST /users — { email, name?, password? | invite?:true }. */
|
|
26
|
+
store(ctx: HttpContext): Promise<void | {
|
|
27
|
+
invited: boolean;
|
|
28
|
+
id: string;
|
|
29
|
+
email: string;
|
|
30
|
+
name: string | null;
|
|
31
|
+
avatarUrl: string | null;
|
|
32
|
+
globalRoles: string[];
|
|
33
|
+
disabled: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
/** PATCH /users/:id — { globalRoles?, name?, avatarUrl? }. */
|
|
36
|
+
update(ctx: HttpContext): Promise<void | {
|
|
37
|
+
id: string;
|
|
38
|
+
email: string;
|
|
39
|
+
name: string | null;
|
|
40
|
+
avatarUrl: string | null;
|
|
41
|
+
globalRoles: string[];
|
|
42
|
+
disabled: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
/** POST /users/:id/disable */
|
|
45
|
+
disable(ctx: HttpContext): Promise<void | {
|
|
46
|
+
id: any;
|
|
47
|
+
disabled: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
/** POST /users/:id/enable */
|
|
50
|
+
enable(ctx: HttpContext): Promise<void | {
|
|
51
|
+
id: any;
|
|
52
|
+
disabled: boolean;
|
|
53
|
+
}>;
|
|
54
|
+
/** POST /users/:id/reset-password — envia o e-mail de reset. */
|
|
55
|
+
resetPassword(ctx: HttpContext): Promise<void | {
|
|
56
|
+
id: any;
|
|
57
|
+
sent: boolean;
|
|
58
|
+
}>;
|
|
59
|
+
/** GET /users/:id/sessions — sessões + grants ativos. */
|
|
60
|
+
sessions(ctx: HttpContext): Promise<{
|
|
61
|
+
canList: boolean;
|
|
62
|
+
sessions: {
|
|
63
|
+
id: string;
|
|
64
|
+
accountId: string;
|
|
65
|
+
loginTs: number | null;
|
|
66
|
+
amr: string[];
|
|
67
|
+
}[];
|
|
68
|
+
grants: {
|
|
69
|
+
id: string;
|
|
70
|
+
accountId: string;
|
|
71
|
+
clientId: string | null;
|
|
72
|
+
accessTokens: number;
|
|
73
|
+
refreshTokens: number;
|
|
74
|
+
}[];
|
|
75
|
+
}>;
|
|
76
|
+
/** POST /users/:id/revoke-sessions — revoga todas as sessões/grants. */
|
|
77
|
+
revokeSessions(ctx: HttpContext): Promise<import("../admin_sessions_service.js").RevokeResult>;
|
|
78
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { AdminUsersService } from './admin_users_service.js';
|
|
3
|
+
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
4
|
+
import { userDto, sessionDto, grantDto, apiError } from './dto.js';
|
|
5
|
+
const PAGE_SIZE = 20;
|
|
6
|
+
/** Lê a config + monta o actor `admin-api` para auditoria. */
|
|
7
|
+
async function ctxBits(ctx) {
|
|
8
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
9
|
+
const cfg = service.config;
|
|
10
|
+
const actor = { actorId: null, ip: ctx.request.ip?.() ?? null, source: 'admin-api' };
|
|
11
|
+
return { service, cfg, actor };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Recurso de usuários da Admin REST API (R6). JSON puro (camelCase), erros no
|
|
15
|
+
* envelope `{ error: { code, message } }`. Toda escrita audita com `actor: 'admin-api'`.
|
|
16
|
+
*/
|
|
17
|
+
export default class ApiUsersController {
|
|
18
|
+
/** GET /users — listagem paginada com busca por e-mail. */
|
|
19
|
+
async index(ctx) {
|
|
20
|
+
const { cfg } = await ctxBits(ctx);
|
|
21
|
+
const search = ctx.request.input('search', '').trim();
|
|
22
|
+
const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
|
|
23
|
+
const limit = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('limit', String(PAGE_SIZE)), 10) || PAGE_SIZE));
|
|
24
|
+
const result = await cfg.accountStore.listAccounts({ search, page, limit });
|
|
25
|
+
const users = new AdminUsersService(cfg);
|
|
26
|
+
const data = await Promise.all(result.data.map(async (u) => userDto(u, await users.isDisabled(u.id))));
|
|
27
|
+
return { data, total: result.total, page, limit };
|
|
28
|
+
}
|
|
29
|
+
/** GET /users/:id */
|
|
30
|
+
async show(ctx) {
|
|
31
|
+
const { cfg } = await ctxBits(ctx);
|
|
32
|
+
const id = ctx.request.param('id');
|
|
33
|
+
const account = await cfg.accountStore.findById(id);
|
|
34
|
+
if (!account)
|
|
35
|
+
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
36
|
+
const disabled = await new AdminUsersService(cfg).isDisabled(id);
|
|
37
|
+
return userDto(account, disabled);
|
|
38
|
+
}
|
|
39
|
+
/** POST /users — { email, name?, password? | invite?:true }. */
|
|
40
|
+
async store(ctx) {
|
|
41
|
+
const { cfg, actor } = await ctxBits(ctx);
|
|
42
|
+
const email = ctx.request.input('email', '').trim();
|
|
43
|
+
const name = ctx.request.input('name') ?? null;
|
|
44
|
+
const password = ctx.request.input('password') ?? null;
|
|
45
|
+
const invite = ctx.request.input('invite') === true || ctx.request.input('invite') === 'true';
|
|
46
|
+
if (!email)
|
|
47
|
+
return ctx.response.badRequest(apiError('invalid_request', 'O campo email é obrigatório.'));
|
|
48
|
+
const users = new AdminUsersService(cfg);
|
|
49
|
+
const result = await users.create(ctx, { email, name, password, invite }, actor);
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
return ctx.response.conflict(apiError('email_taken', 'Já existe uma conta com este e-mail.'));
|
|
52
|
+
}
|
|
53
|
+
ctx.response.status(201);
|
|
54
|
+
return { ...userDto(result.account), invited: result.invited };
|
|
55
|
+
}
|
|
56
|
+
/** PATCH /users/:id — { globalRoles?, name?, avatarUrl? }. */
|
|
57
|
+
async update(ctx) {
|
|
58
|
+
const { cfg } = await ctxBits(ctx);
|
|
59
|
+
const id = ctx.request.param('id');
|
|
60
|
+
const account = await cfg.accountStore.findById(id);
|
|
61
|
+
if (!account)
|
|
62
|
+
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
63
|
+
const users = new AdminUsersService(cfg);
|
|
64
|
+
const roles = ctx.request.input('globalRoles');
|
|
65
|
+
if (Array.isArray(roles)) {
|
|
66
|
+
const normalized = Array.from(new Set(roles.map((r) => String(r).trim()).filter(Boolean)));
|
|
67
|
+
await users.setGlobalRoles(id, normalized);
|
|
68
|
+
}
|
|
69
|
+
const name = ctx.request.input('name');
|
|
70
|
+
const avatarUrl = ctx.request.input('avatarUrl');
|
|
71
|
+
if (name !== undefined || avatarUrl !== undefined) {
|
|
72
|
+
const patch = {};
|
|
73
|
+
if (name !== undefined)
|
|
74
|
+
patch.name = name;
|
|
75
|
+
if (avatarUrl !== undefined)
|
|
76
|
+
patch.avatarUrl = avatarUrl;
|
|
77
|
+
await users.updateProfile(id, patch);
|
|
78
|
+
}
|
|
79
|
+
const updated = await cfg.accountStore.findById(id);
|
|
80
|
+
return userDto(updated, await users.isDisabled(id));
|
|
81
|
+
}
|
|
82
|
+
/** POST /users/:id/disable */
|
|
83
|
+
async disable(ctx) {
|
|
84
|
+
return this.#setStatus(ctx, true);
|
|
85
|
+
}
|
|
86
|
+
/** POST /users/:id/enable */
|
|
87
|
+
async enable(ctx) {
|
|
88
|
+
return this.#setStatus(ctx, false);
|
|
89
|
+
}
|
|
90
|
+
async #setStatus(ctx, disable) {
|
|
91
|
+
const { cfg, actor } = await ctxBits(ctx);
|
|
92
|
+
const id = ctx.request.param('id');
|
|
93
|
+
const applied = await new AdminUsersService(cfg).setStatus(id, disable, actor);
|
|
94
|
+
if (!applied) {
|
|
95
|
+
return ctx.response.conflict(apiError('capability_unsupported', 'O store de contas não suporta habilitar/desabilitar.'));
|
|
96
|
+
}
|
|
97
|
+
return { id, disabled: disable };
|
|
98
|
+
}
|
|
99
|
+
/** POST /users/:id/reset-password — envia o e-mail de reset. */
|
|
100
|
+
async resetPassword(ctx) {
|
|
101
|
+
const { cfg, actor } = await ctxBits(ctx);
|
|
102
|
+
const id = ctx.request.param('id');
|
|
103
|
+
const account = await new AdminUsersService(cfg).resetPassword(ctx, id, actor);
|
|
104
|
+
if (!account)
|
|
105
|
+
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
106
|
+
return { id, sent: true };
|
|
107
|
+
}
|
|
108
|
+
/** GET /users/:id/sessions — sessões + grants ativos. */
|
|
109
|
+
async sessions(ctx) {
|
|
110
|
+
const { service } = await ctxBits(ctx);
|
|
111
|
+
const id = ctx.request.param('id');
|
|
112
|
+
const admin = new AdminSessionsService(service);
|
|
113
|
+
const sessions = await admin.listSessions(id);
|
|
114
|
+
const grants = await admin.listGrants(id);
|
|
115
|
+
return {
|
|
116
|
+
canList: admin.canList,
|
|
117
|
+
sessions: sessions.map(sessionDto),
|
|
118
|
+
grants: grants.map(grantDto),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** POST /users/:id/revoke-sessions — revoga todas as sessões/grants. */
|
|
122
|
+
async revokeSessions(ctx) {
|
|
123
|
+
const { service, cfg, actor } = await ctxBits(ctx);
|
|
124
|
+
const id = ctx.request.param('id');
|
|
125
|
+
const result = await new AdminSessionsService(service).revokeAll(id);
|
|
126
|
+
await cfg.audit?.record({
|
|
127
|
+
type: 'session.revoked_all',
|
|
128
|
+
accountId: id,
|
|
129
|
+
actorId: actor.actorId,
|
|
130
|
+
ip: actor.ip,
|
|
131
|
+
metadata: { actor: actor.source, ...result },
|
|
132
|
+
});
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AuthAccount } from '../../accounts/account_store.js';
|
|
2
|
+
import type { AdminClient, CreatedClient } from '../admin_clients_service.js';
|
|
3
|
+
import type { AdminSession, AdminGrant } from '../admin_sessions_service.js';
|
|
4
|
+
import type { StoredAuditEvent } from '../../audit/audit_sink.js';
|
|
5
|
+
/** Projeta uma conta para a forma JSON (camelCase) da Admin REST API. */
|
|
6
|
+
export declare function userDto(account: AuthAccount, disabled?: boolean): {
|
|
7
|
+
id: string;
|
|
8
|
+
email: string;
|
|
9
|
+
name: string | null;
|
|
10
|
+
avatarUrl: string | null;
|
|
11
|
+
globalRoles: string[];
|
|
12
|
+
disabled: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function clientDto(client: AdminClient): {
|
|
15
|
+
clientId: string;
|
|
16
|
+
confidential: boolean;
|
|
17
|
+
grants: string[];
|
|
18
|
+
redirectUris: string[];
|
|
19
|
+
postLogoutRedirectUris: string[];
|
|
20
|
+
tokenEndpointAuthMethod: string;
|
|
21
|
+
};
|
|
22
|
+
/** Inclui o secret (mostrado UMA vez) em create/regenerate. */
|
|
23
|
+
export declare function createdClientDto(created: CreatedClient): {
|
|
24
|
+
clientId: string;
|
|
25
|
+
clientSecret: string | null;
|
|
26
|
+
};
|
|
27
|
+
export declare function sessionDto(session: AdminSession): {
|
|
28
|
+
id: string;
|
|
29
|
+
accountId: string;
|
|
30
|
+
loginTs: number | null;
|
|
31
|
+
amr: string[];
|
|
32
|
+
};
|
|
33
|
+
export declare function grantDto(grant: AdminGrant): {
|
|
34
|
+
id: string;
|
|
35
|
+
accountId: string;
|
|
36
|
+
clientId: string | null;
|
|
37
|
+
accessTokens: number;
|
|
38
|
+
refreshTokens: number;
|
|
39
|
+
};
|
|
40
|
+
export declare function auditDto(event: StoredAuditEvent): {
|
|
41
|
+
id: string;
|
|
42
|
+
type: import("../../audit/audit_sink.js").AuditEventType;
|
|
43
|
+
accountId: string | null;
|
|
44
|
+
email: string | null;
|
|
45
|
+
clientId: string | null;
|
|
46
|
+
actorId: string | null;
|
|
47
|
+
ip: string | null;
|
|
48
|
+
metadata: Record<string, unknown> | null;
|
|
49
|
+
createdAt: string | null;
|
|
50
|
+
};
|
|
51
|
+
/** Envelope de erro padrão da Admin REST API. */
|
|
52
|
+
export declare function apiError(code: string, message: string): {
|
|
53
|
+
error: {
|
|
54
|
+
code: string;
|
|
55
|
+
message: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Projeta uma conta para a forma JSON (camelCase) da Admin REST API. */
|
|
2
|
+
export function userDto(account, disabled = false) {
|
|
3
|
+
return {
|
|
4
|
+
id: account.id,
|
|
5
|
+
email: account.email,
|
|
6
|
+
name: account.name ?? null,
|
|
7
|
+
avatarUrl: account.avatarUrl ?? null,
|
|
8
|
+
globalRoles: account.globalRoles ?? [],
|
|
9
|
+
disabled,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function clientDto(client) {
|
|
13
|
+
return {
|
|
14
|
+
clientId: client.clientId,
|
|
15
|
+
confidential: client.confidential,
|
|
16
|
+
grants: client.grants,
|
|
17
|
+
redirectUris: client.redirectUris,
|
|
18
|
+
postLogoutRedirectUris: client.postLogoutRedirectUris,
|
|
19
|
+
tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Inclui o secret (mostrado UMA vez) em create/regenerate. */
|
|
23
|
+
export function createdClientDto(created) {
|
|
24
|
+
return {
|
|
25
|
+
clientId: created.clientId,
|
|
26
|
+
clientSecret: created.clientSecret ?? null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function sessionDto(session) {
|
|
30
|
+
return {
|
|
31
|
+
id: session.id,
|
|
32
|
+
accountId: session.accountId,
|
|
33
|
+
loginTs: session.loginTs ?? null,
|
|
34
|
+
amr: session.amr ?? [],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function grantDto(grant) {
|
|
38
|
+
return {
|
|
39
|
+
id: grant.id,
|
|
40
|
+
accountId: grant.accountId,
|
|
41
|
+
clientId: grant.clientId ?? null,
|
|
42
|
+
accessTokens: grant.accessTokens,
|
|
43
|
+
refreshTokens: grant.refreshTokens,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function auditDto(event) {
|
|
47
|
+
return {
|
|
48
|
+
id: event.id,
|
|
49
|
+
type: event.type,
|
|
50
|
+
accountId: event.accountId ?? null,
|
|
51
|
+
email: event.email ?? null,
|
|
52
|
+
clientId: event.clientId ?? null,
|
|
53
|
+
actorId: event.actorId ?? null,
|
|
54
|
+
ip: event.ip ?? null,
|
|
55
|
+
metadata: event.metadata ?? null,
|
|
56
|
+
createdAt: event.createdAt instanceof Date ? event.createdAt.toISOString() : event.createdAt,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Envelope de erro padrão da Admin REST API. */
|
|
60
|
+
export function apiError(code, message) {
|
|
61
|
+
return { error: { code, message } };
|
|
62
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ResolvedServerConfig } from '../../define_config.js';
|
|
2
|
+
/** Resultado de introspecção genérica (PAT ou opaque access token). */
|
|
3
|
+
export type VerifyResult = {
|
|
4
|
+
active: false;
|
|
5
|
+
} | {
|
|
6
|
+
active: true;
|
|
7
|
+
/** 'pat' (Personal Access Token) ou 'access_token' (opaque AT do provider). */
|
|
8
|
+
tokenType: 'pat' | 'access_token';
|
|
9
|
+
sub: string;
|
|
10
|
+
email?: string | null;
|
|
11
|
+
name?: string | null;
|
|
12
|
+
roles?: string[];
|
|
13
|
+
scopes?: string[];
|
|
14
|
+
audience?: string | string[] | null;
|
|
15
|
+
clientId?: string | null;
|
|
16
|
+
exp?: number | null;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Introspecção genérica de token usada pela Admin REST API (`POST /tokens/verify`).
|
|
20
|
+
* Roteia por prefixo: tokens `pat_...` vão pelo {@link PatStore} (mesma rota do
|
|
21
|
+
* `/authkit/pat/introspect`); os demais são tratados como opaque access tokens e
|
|
22
|
+
* resolvidos pelo `AccessToken.find` do oidc-provider. Sempre best-effort: token
|
|
23
|
+
* desconhecido/expirado → `{ active: false }`.
|
|
24
|
+
*/
|
|
25
|
+
export declare class TokenVerifyService {
|
|
26
|
+
#private;
|
|
27
|
+
private cfg;
|
|
28
|
+
/** Provider do oidc-provider (service.provider) — para opaque AT. */
|
|
29
|
+
private provider;
|
|
30
|
+
constructor(cfg: ResolvedServerConfig,
|
|
31
|
+
/** Provider do oidc-provider (service.provider) — para opaque AT. */
|
|
32
|
+
provider: any);
|
|
33
|
+
verify(token: string): Promise<VerifyResult>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Introspecção genérica de token usada pela Admin REST API (`POST /tokens/verify`).
|
|
3
|
+
* Roteia por prefixo: tokens `pat_...` vão pelo {@link PatStore} (mesma rota do
|
|
4
|
+
* `/authkit/pat/introspect`); os demais são tratados como opaque access tokens e
|
|
5
|
+
* resolvidos pelo `AccessToken.find` do oidc-provider. Sempre best-effort: token
|
|
6
|
+
* desconhecido/expirado → `{ active: false }`.
|
|
7
|
+
*/
|
|
8
|
+
export class TokenVerifyService {
|
|
9
|
+
cfg;
|
|
10
|
+
provider;
|
|
11
|
+
constructor(cfg,
|
|
12
|
+
/** Provider do oidc-provider (service.provider) — para opaque AT. */
|
|
13
|
+
provider) {
|
|
14
|
+
this.cfg = cfg;
|
|
15
|
+
this.provider = provider;
|
|
16
|
+
}
|
|
17
|
+
async verify(token) {
|
|
18
|
+
if (!token || typeof token !== 'string')
|
|
19
|
+
return { active: false };
|
|
20
|
+
if (token.startsWith('pat_'))
|
|
21
|
+
return this.#verifyPat(token);
|
|
22
|
+
return this.#verifyAccessToken(token);
|
|
23
|
+
}
|
|
24
|
+
async #verifyPat(token) {
|
|
25
|
+
if (!this.cfg.patStore)
|
|
26
|
+
return { active: false };
|
|
27
|
+
const meta = await this.cfg.patStore.findActiveByToken(token);
|
|
28
|
+
if (!meta)
|
|
29
|
+
return { active: false };
|
|
30
|
+
const account = await this.cfg.accountStore.findById(meta.accountId);
|
|
31
|
+
if (!account)
|
|
32
|
+
return { active: false };
|
|
33
|
+
return {
|
|
34
|
+
active: true,
|
|
35
|
+
tokenType: 'pat',
|
|
36
|
+
sub: account.id,
|
|
37
|
+
email: account.email,
|
|
38
|
+
name: account.name ?? null,
|
|
39
|
+
roles: account.globalRoles ?? [],
|
|
40
|
+
scopes: meta.scopes,
|
|
41
|
+
audience: meta.audience,
|
|
42
|
+
exp: meta.exp,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async #verifyAccessToken(token) {
|
|
46
|
+
let at;
|
|
47
|
+
try {
|
|
48
|
+
at = await this.provider?.AccessToken?.find(token);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { active: false };
|
|
52
|
+
}
|
|
53
|
+
if (!at)
|
|
54
|
+
return { active: false };
|
|
55
|
+
// O oidc-provider expira artefatos sozinho; isExpired/exp como guarda extra.
|
|
56
|
+
if (typeof at.isExpired === 'boolean' && at.isExpired)
|
|
57
|
+
return { active: false };
|
|
58
|
+
const sub = at.accountId ?? '';
|
|
59
|
+
const account = sub ? await this.cfg.accountStore.findById(sub) : null;
|
|
60
|
+
const scopes = typeof at.scope === 'string' ? at.scope.split(' ').filter(Boolean) : [];
|
|
61
|
+
return {
|
|
62
|
+
active: true,
|
|
63
|
+
tokenType: 'access_token',
|
|
64
|
+
sub,
|
|
65
|
+
email: account?.email ?? null,
|
|
66
|
+
name: account?.name ?? null,
|
|
67
|
+
roles: account?.globalRoles ?? [],
|
|
68
|
+
scopes,
|
|
69
|
+
audience: at.aud ?? null,
|
|
70
|
+
clientId: at.clientId ?? null,
|
|
71
|
+
exp: typeof at.exp === 'number' ? at.exp : null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import '../../augmentations.js';
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
3
2
|
import { supportsAccountStatus } from '../../../accounts/account_store.js';
|
|
4
3
|
import { ACCOUNT_SESSION_KEY } from '../../middleware/account_auth.js';
|
|
5
4
|
import { adminCreateUserValidator } from '../../validators.js';
|
|
6
|
-
import {
|
|
5
|
+
import { AdminUsersService } from '../../admin_api/admin_users_service.js';
|
|
7
6
|
const PAGE_SIZE = 20;
|
|
8
7
|
/**
|
|
9
8
|
* Gestão de usuários do IdP: listagem paginada com busca por e-mail e edição das
|
|
@@ -57,30 +56,15 @@ export default class AdminUsersController {
|
|
|
57
56
|
async store(ctx) {
|
|
58
57
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
59
58
|
const cfg = service.config;
|
|
60
|
-
const store = cfg.accountStore;
|
|
61
59
|
const actorId = ctx.session.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
62
60
|
const ip = ctx.request.ip?.() ?? null;
|
|
63
61
|
const { email, name, password } = await ctx.request.validateUsing(adminCreateUserValidator);
|
|
64
|
-
const
|
|
65
|
-
|
|
62
|
+
const users = new AdminUsersService(cfg);
|
|
63
|
+
const result = await users.create(ctx, { email, name, password }, { actorId, ip });
|
|
64
|
+
if (!result.ok) {
|
|
66
65
|
ctx.session.flash('usersError', cfg.messages['errors.email_taken'] ?? 'errors.email_taken');
|
|
67
66
|
return ctx.response.redirect('/admin/users');
|
|
68
67
|
}
|
|
69
|
-
// Sem senha informada: cria com uma senha aleatória forte (descartável) e
|
|
70
|
-
// dispara o fluxo de reset para o usuário definir a sua.
|
|
71
|
-
const initialPassword = password ?? randomBytes(24).toString('hex');
|
|
72
|
-
const account = await store.create({ email, password: initialPassword, fullName: name ?? null });
|
|
73
|
-
await cfg.audit?.record({
|
|
74
|
-
type: 'user.created',
|
|
75
|
-
accountId: account.id,
|
|
76
|
-
email,
|
|
77
|
-
actorId,
|
|
78
|
-
ip,
|
|
79
|
-
metadata: { invited: !password },
|
|
80
|
-
});
|
|
81
|
-
if (!password) {
|
|
82
|
-
await this.#sendResetEmail(ctx, cfg, email);
|
|
83
|
-
}
|
|
84
68
|
ctx.session.flash('userCreated', cfg.messages['admin.users.created'] ?? 'admin.users.created');
|
|
85
69
|
return ctx.response.redirect('/admin/users');
|
|
86
70
|
}
|
|
@@ -88,21 +72,10 @@ export default class AdminUsersController {
|
|
|
88
72
|
async resetPassword(ctx) {
|
|
89
73
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
90
74
|
const cfg = service.config;
|
|
91
|
-
const store = cfg.accountStore;
|
|
92
75
|
const actorId = ctx.session.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
93
76
|
const ip = ctx.request.ip?.() ?? null;
|
|
94
77
|
const accountId = ctx.request.param('id');
|
|
95
|
-
|
|
96
|
-
if (account) {
|
|
97
|
-
await this.#sendResetEmail(ctx, cfg, account.email);
|
|
98
|
-
await cfg.audit?.record({
|
|
99
|
-
type: 'user.password_reset_sent',
|
|
100
|
-
accountId,
|
|
101
|
-
email: account.email,
|
|
102
|
-
actorId,
|
|
103
|
-
ip,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
78
|
+
await new AdminUsersService(cfg).resetPassword(ctx, accountId, { actorId, ip });
|
|
106
79
|
ctx.session.flash('resetSent', cfg.messages['admin.users.reset_sent'] ?? 'admin.users.reset_sent');
|
|
107
80
|
return this.#redirectBack(ctx);
|
|
108
81
|
}
|
|
@@ -117,40 +90,16 @@ export default class AdminUsersController {
|
|
|
117
90
|
async #toggleStatus(ctx, disable) {
|
|
118
91
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
119
92
|
const cfg = service.config;
|
|
120
|
-
const store = cfg.accountStore;
|
|
121
93
|
const actorId = ctx.session.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
122
94
|
const ip = ctx.request.ip?.() ?? null;
|
|
123
95
|
const accountId = ctx.request.param('id');
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await store.disableAccount(accountId);
|
|
127
|
-
else
|
|
128
|
-
await store.enableAccount(accountId);
|
|
129
|
-
await cfg.audit?.record({
|
|
130
|
-
type: disable ? 'user.disabled' : 'user.enabled',
|
|
131
|
-
accountId,
|
|
132
|
-
actorId,
|
|
133
|
-
ip,
|
|
134
|
-
});
|
|
96
|
+
const applied = await new AdminUsersService(cfg).setStatus(accountId, disable, { actorId, ip });
|
|
97
|
+
if (applied) {
|
|
135
98
|
ctx.session.flash('statusChanged', cfg.messages[disable ? 'admin.users.disabled' : 'admin.users.enabled'] ??
|
|
136
99
|
(disable ? 'admin.users.disabled' : 'admin.users.enabled'));
|
|
137
100
|
}
|
|
138
101
|
return this.#redirectBack(ctx);
|
|
139
102
|
}
|
|
140
|
-
/** Emite o token de reset e dispara o e-mail (hook do config tem prioridade). */
|
|
141
|
-
async #sendResetEmail(ctx, cfg, email) {
|
|
142
|
-
const issued = await cfg.accountStore.issuePasswordResetToken(email);
|
|
143
|
-
if (!issued)
|
|
144
|
-
return;
|
|
145
|
-
const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
|
|
146
|
-
const resetUrl = `${origin}/auth/reset-password?token=${encodeURIComponent(issued.token)}`;
|
|
147
|
-
if (cfg.mail?.onPasswordReset) {
|
|
148
|
-
await cfg.mail.onPasswordReset({ email, resetUrl, token: issued.token });
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
await sendPasswordResetEmail(ctx, { email, resetUrl });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
103
|
#redirectBack(ctx) {
|
|
155
104
|
const search = ctx.request.input('search', '').trim();
|
|
156
105
|
const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
|
|
@@ -46,6 +46,13 @@ export interface AuthHostOptions {
|
|
|
46
46
|
* Espelhe o `admin.enabled` de config/authkit.ts.
|
|
47
47
|
*/
|
|
48
48
|
admin?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Admin REST API opt-in (R6); quando `true`, monta o grupo `/api/authkit/v1/*`
|
|
51
|
+
* atrás do `adminApiGuard` (API key). Necessário aqui (e não só no config) porque
|
|
52
|
+
* a decisão de montar as rotas é tomada em tempo de registro, antes do config
|
|
53
|
+
* (lazy) resolver. Espelhe o `adminApi.enabled` de config/authkit.ts.
|
|
54
|
+
*/
|
|
55
|
+
adminApi?: boolean;
|
|
49
56
|
}
|
|
50
57
|
/**
|
|
51
58
|
* Monta todas as rotas do host-kit do Authorization Server numa chamada.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveRateLimit } from '../define_config.js';
|
|
2
2
|
import { createAuthThrottles } from './rate_limit.js';
|
|
3
3
|
import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
|
|
4
|
+
import { adminApiGuard } from './admin_api/admin_api_guard.js';
|
|
4
5
|
/**
|
|
5
6
|
* Guard inline do console de conta. Usamos uma closure (forma confiável do
|
|
6
7
|
* `.use()` do AdonisJS) em vez de `() => import(middleware)` — a forma lazy de
|
|
@@ -60,6 +61,9 @@ const C = {
|
|
|
60
61
|
adminSessions: () => import('./controllers/admin/admin_sessions_controller.js'),
|
|
61
62
|
adminClients: () => import('./controllers/admin/admin_clients_controller.js'),
|
|
62
63
|
adminAudit: () => import('./controllers/admin/admin_audit_controller.js'),
|
|
64
|
+
apiUsers: () => import('./admin_api/api_users_controller.js'),
|
|
65
|
+
apiClients: () => import('./admin_api/api_clients_controller.js'),
|
|
66
|
+
apiMisc: () => import('./admin_api/api_misc_controller.js'),
|
|
63
67
|
};
|
|
64
68
|
/**
|
|
65
69
|
* Monta todas as rotas do host-kit do Authorization Server numa chamada.
|
|
@@ -174,4 +178,39 @@ export function registerAuthHost(router, opts) {
|
|
|
174
178
|
})
|
|
175
179
|
.use([adminGuard]);
|
|
176
180
|
}
|
|
181
|
+
// Admin REST API (opt-in — R6). Superfície machine-to-machine atrás do
|
|
182
|
+
// adminApiGuard (API key). Todas as rotas levam o throttle de introspecção.
|
|
183
|
+
if (opts.adminApi) {
|
|
184
|
+
// Aplica o throttle de introspecção a uma rota qualquer (GET ou escrita).
|
|
185
|
+
const withApiThrottle = (route) => {
|
|
186
|
+
if (throttles)
|
|
187
|
+
route.use([throttles.introspection]);
|
|
188
|
+
return route;
|
|
189
|
+
};
|
|
190
|
+
router
|
|
191
|
+
.group(() => {
|
|
192
|
+
// Usuários.
|
|
193
|
+
withApiThrottle(router.get('/users', [C.apiUsers, 'index']));
|
|
194
|
+
withApiThrottle(router.post('/users', [C.apiUsers, 'store']));
|
|
195
|
+
withApiThrottle(router.get('/users/:id', [C.apiUsers, 'show']));
|
|
196
|
+
withApiThrottle(router.patch('/users/:id', [C.apiUsers, 'update']));
|
|
197
|
+
withApiThrottle(router.post('/users/:id/disable', [C.apiUsers, 'disable']));
|
|
198
|
+
withApiThrottle(router.post('/users/:id/enable', [C.apiUsers, 'enable']));
|
|
199
|
+
withApiThrottle(router.post('/users/:id/reset-password', [C.apiUsers, 'resetPassword']));
|
|
200
|
+
withApiThrottle(router.get('/users/:id/sessions', [C.apiUsers, 'sessions']));
|
|
201
|
+
withApiThrottle(router.post('/users/:id/revoke-sessions', [C.apiUsers, 'revokeSessions']));
|
|
202
|
+
// Clients OIDC.
|
|
203
|
+
withApiThrottle(router.get('/clients', [C.apiClients, 'index']));
|
|
204
|
+
withApiThrottle(router.post('/clients', [C.apiClients, 'store']));
|
|
205
|
+
withApiThrottle(router.get('/clients/:id', [C.apiClients, 'show']));
|
|
206
|
+
withApiThrottle(router.patch('/clients/:id', [C.apiClients, 'update']));
|
|
207
|
+
withApiThrottle(router.post('/clients/:id/regenerate-secret', [C.apiClients, 'regenerateSecret']));
|
|
208
|
+
withApiThrottle(router.delete('/clients/:id', [C.apiClients, 'destroy']));
|
|
209
|
+
// Auditoria + verificação de token.
|
|
210
|
+
withApiThrottle(router.get('/audit', [C.apiMisc, 'audit']));
|
|
211
|
+
withApiThrottle(router.post('/tokens/verify', [C.apiMisc, 'verify']));
|
|
212
|
+
})
|
|
213
|
+
.prefix('/api/authkit/v1')
|
|
214
|
+
.use([adminApiGuard]);
|
|
215
|
+
}
|
|
177
216
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/adonis-authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|