@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.
- package/build/host/views/account/security.edge +14 -1
- package/build/index.d.ts +14 -2
- package/build/index.js +9 -1
- package/build/src/define_config.d.ts +54 -0
- package/build/src/define_config.js +17 -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/avatar_storage.d.ts +58 -0
- package/build/src/host/avatar_storage.js +125 -0
- package/build/src/host/controllers/account_security_controller.js +33 -1
- package/build/src/host/controllers/admin/admin_users_controller.js +7 -58
- package/build/src/host/i18n.d.ts +8 -0
- package/build/src/host/i18n.js +8 -0
- package/build/src/host/register_auth_host.d.ts +7 -0
- package/build/src/host/register_auth_host.js +39 -0
- 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
|
+
}
|