@dudousxd/adonis-authkit-server 0.6.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.
Files changed (28) hide show
  1. package/build/host/views/account/security.edge +14 -1
  2. package/build/index.d.ts +14 -2
  3. package/build/index.js +9 -1
  4. package/build/src/define_config.d.ts +54 -0
  5. package/build/src/define_config.js +17 -0
  6. package/build/src/host/admin_api/admin_api_guard.d.ts +8 -0
  7. package/build/src/host/admin_api/admin_api_guard.js +41 -0
  8. package/build/src/host/admin_api/admin_users_service.d.ts +59 -0
  9. package/build/src/host/admin_api/admin_users_service.js +112 -0
  10. package/build/src/host/admin_api/api_clients_controller.d.ts +48 -0
  11. package/build/src/host/admin_api/api_clients_controller.js +104 -0
  12. package/build/src/host/admin_api/api_misc_controller.d.ts +17 -0
  13. package/build/src/host/admin_api/api_misc_controller.js +37 -0
  14. package/build/src/host/admin_api/api_users_controller.d.ts +78 -0
  15. package/build/src/host/admin_api/api_users_controller.js +135 -0
  16. package/build/src/host/admin_api/dto.d.ts +57 -0
  17. package/build/src/host/admin_api/dto.js +62 -0
  18. package/build/src/host/admin_api/token_verify_service.d.ts +34 -0
  19. package/build/src/host/admin_api/token_verify_service.js +74 -0
  20. package/build/src/host/avatar_storage.d.ts +58 -0
  21. package/build/src/host/avatar_storage.js +125 -0
  22. package/build/src/host/controllers/account_security_controller.js +33 -1
  23. package/build/src/host/controllers/admin/admin_users_controller.js +7 -58
  24. package/build/src/host/i18n.d.ts +8 -0
  25. package/build/src/host/i18n.js +8 -0
  26. package/build/src/host/register_auth_host.d.ts +7 -0
  27. package/build/src/host/register_auth_host.js +39 -0
  28. package/package.json +6 -1
@@ -33,13 +33,26 @@
33
33
  <div class="mb-6 rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
34
34
  <h2 class="mb-1 text-sm font-semibold text-gray-900">{{ t('account.profile.section') }}</h2>
35
35
  <p class="mb-4 text-xs text-gray-500">{{ t('account.profile.intro') }}</p>
36
- <form method="POST" action="/account/security/profile" class="space-y-4">
36
+ <form method="POST" action="/account/security/profile" enctype="multipart/form-data" class="space-y-4">
37
37
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
38
+ @if(avatarUrl)
39
+ <div>
40
+ <img src="{{ avatarUrl }}" alt="avatar" class="h-16 w-16 rounded-full object-cover ring-1 ring-black/5" />
41
+ </div>
42
+ @end
38
43
  <div>
39
44
  <label class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.profile.name_label') }}</label>
40
45
  <input name="name" type="text" value="{{ name }}" maxlength="255"
41
46
  class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
42
47
  </div>
48
+ @if(avatarUploadSupported)
49
+ <div>
50
+ <label class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.profile.avatar_upload_label') }}</label>
51
+ <input name="avatar" type="file" accept="image/jpeg,image/png,image/webp"
52
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
53
+ <p class="mt-1 text-xs text-gray-500">{{ t('account.profile.avatar_upload_hint') }}</p>
54
+ </div>
55
+ @end
43
56
  <div>
44
57
  <label class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.profile.avatar_label') }}</label>
45
58
  <input name="avatarUrl" type="url" value="{{ avatarUrl }}"
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.
@@ -161,6 +161,31 @@ export interface ResolvedDeviceFlowConfig {
161
161
  enabled: boolean;
162
162
  }
163
163
  export declare function resolveDeviceFlow(input?: DeviceFlowConfigInput): ResolvedDeviceFlowConfig;
164
+ /**
165
+ * Uploads — usa o `@adonisjs/drive` JÁ configurado no app (mesmo princípio do
166
+ * mailer/limiter: a infra do host por padrão, sobreponível aqui). Hoje cobre o
167
+ * upload de avatar no console de conta. Se o drive estiver ausente, a feature
168
+ * degrada para o input de URL.
169
+ */
170
+ export interface UploadsConfigInput {
171
+ avatars?: {
172
+ /** Disk do `@adonisjs/drive` a usar. Default: o disk DEFAULT do app. */
173
+ disk?: string;
174
+ /** Diretório/prefixo das chaves. Default: 'authkit/avatars'. */
175
+ directory?: string;
176
+ /** Tamanho máximo em MB. Default: 5. */
177
+ maxSizeMb?: number;
178
+ };
179
+ }
180
+ export interface ResolvedUploadsConfig {
181
+ avatars: {
182
+ /** Disk explícito; `undefined` = disk DEFAULT do app. */
183
+ disk?: string;
184
+ directory: string;
185
+ maxSizeMb: number;
186
+ };
187
+ }
188
+ export declare function resolveUploads(input?: UploadsConfigInput): ResolvedUploadsConfig;
164
189
  /**
165
190
  * DPoP — Demonstrating Proof of Possession (RFC 9449). Quando habilitado, o
166
191
  * oidc-provider aceita DPoP proofs e emite tokens sender-constrained
@@ -253,6 +278,23 @@ export interface ResolvedAdminConfig {
253
278
  roles: string[];
254
279
  }
255
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;
256
298
  /**
257
299
  * Parâmetros do Relying Party (RP) das cerimônias WebAuthn / passkeys. Quando
258
300
  * omitidos, são derivados do `issuer`: `rpId` = hostname (sem porta), `origin` =
@@ -337,6 +379,8 @@ export interface AuthServerConfigInput {
337
379
  dynamicRegistration?: DynamicRegistrationConfigInput;
338
380
  /** Device Authorization Grant (RFC 8628). Default: desligado. */
339
381
  deviceFlow?: DeviceFlowConfigInput;
382
+ /** Uploads (avatar) via o `@adonisjs/drive` do app. Default: drive default, 5MB. */
383
+ uploads?: UploadsConfigInput;
340
384
  /** DPoP — sender-constrained tokens (RFC 9449). Default: desligado. */
341
385
  dpop?: DpopConfigInput;
342
386
  /** Pushed Authorization Requests (RFC 9126). Default: desligado. */
@@ -360,6 +404,12 @@ export interface AuthServerConfigInput {
360
404
  * (a montagem das rotas acontece antes do config resolver).
361
405
  */
362
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;
363
413
  }
364
414
  export interface ResolvedServerConfig {
365
415
  issuer: string;
@@ -401,6 +451,8 @@ export interface ResolvedServerConfig {
401
451
  dynamicRegistration: ResolvedDynamicRegistrationConfig;
402
452
  /** Device Authorization Grant resolvido (default desligado). */
403
453
  deviceFlow: ResolvedDeviceFlowConfig;
454
+ /** Uploads resolvido (avatar via drive do app; sempre presente). */
455
+ uploads: ResolvedUploadsConfig;
404
456
  /** DPoP resolvido (default desligado). */
405
457
  dpop: ResolvedDpopConfig;
406
458
  /** PAR resolvido (default desligado). */
@@ -413,6 +465,8 @@ export interface ResolvedServerConfig {
413
465
  passwordless: ResolvedPasswordlessConfig;
414
466
  /** Console admin resolvido (sempre presente; default desligado). */
415
467
  admin: ResolvedAdminConfig;
468
+ /** Admin REST API resolvida (sempre presente; default desligada). */
469
+ adminApi: ResolvedAdminApiConfig;
416
470
  /** Catálogo de mensagens ativo (locale resolvido), pronto para os renderers. */
417
471
  messages: AuthMessages;
418
472
  /** Locale ativo (default 'pt-BR'). */
@@ -56,6 +56,15 @@ export function resolveDynamicRegistration(input) {
56
56
  export function resolveDeviceFlow(input) {
57
57
  return { enabled: input?.enabled ?? false };
58
58
  }
59
+ export function resolveUploads(input) {
60
+ return {
61
+ avatars: {
62
+ disk: input?.avatars?.disk,
63
+ directory: input?.avatars?.directory ?? 'authkit/avatars',
64
+ maxSizeMb: input?.avatars?.maxSizeMb ?? 5,
65
+ },
66
+ };
67
+ }
59
68
  export function resolveDpop(input) {
60
69
  return { enabled: input?.enabled ?? false };
61
70
  }
@@ -83,6 +92,12 @@ export function resolveAdmin(input) {
83
92
  roles: input?.roles && input.roles.length > 0 ? input.roles : ['ADMIN'],
84
93
  };
85
94
  }
95
+ export function resolveAdminApi(input) {
96
+ return {
97
+ enabled: input?.enabled ?? false,
98
+ apiKeys: input?.apiKeys ?? [],
99
+ };
100
+ }
86
101
  /**
87
102
  * Resolve os parâmetros do RP de WebAuthn a partir do `issuer` quando omitidos.
88
103
  * `rpName` cai no `fallbackName` (branding/mfaIssuer) quando ausente.
@@ -173,12 +188,14 @@ export function defineConfig(config) {
173
188
  webauthn: resolveWebauthn(config.issuer, config.mfaIssuer ?? 'AuthKit', config.webauthn),
174
189
  dynamicRegistration: resolveDynamicRegistration(config.dynamicRegistration),
175
190
  deviceFlow: resolveDeviceFlow(config.deviceFlow),
191
+ uploads: resolveUploads(config.uploads),
176
192
  dpop: resolveDpop(config.dpop),
177
193
  par: resolvePar(config.par),
178
194
  stepUp: resolveStepUp(config.stepUp),
179
195
  trustedDevices: resolveTrustedDevices(config.trustedDevices),
180
196
  passwordless: resolvePasswordless(config.passwordless),
181
197
  admin: resolveAdmin(config.admin),
198
+ adminApi: resolveAdminApi(config.adminApi),
182
199
  messages: resolveMessages(config.i18n),
183
200
  locale: config.i18n?.locale ?? 'pt-BR',
184
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
+ }