@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
@@ -3,6 +3,7 @@ import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
3
3
  import { supportsAccountSecurity, supportsProfile } from '../../accounts/account_store.js';
4
4
  import { changePasswordValidator, changeEmailValidator, updateProfileValidator } from '../validators.js';
5
5
  import { sendEmailChangeConfirmationEmail } from '../default_mailer.js';
6
+ import { storeAvatar, isDriveAvailable, AvatarUploadError } from '../avatar_storage.js';
6
7
  import { translate } from '../i18n.js';
7
8
  import { TRUSTED_DEVICE_COOKIE } from '../trusted_device.js';
8
9
  /**
@@ -23,6 +24,8 @@ export default class AccountSecurityController {
23
24
  csrfToken: ctx.request.csrfToken,
24
25
  supported: supportsAccountSecurity(cfg.accountStore),
25
26
  profileSupported: supportsProfile(cfg.accountStore),
27
+ // Só mostramos o input de arquivo se o drive do app estiver disponível.
28
+ avatarUploadSupported: await isDriveAvailable(),
26
29
  email: account?.email ?? '',
27
30
  name: account?.name ?? '',
28
31
  avatarUrl: account?.avatarUrl ?? '',
@@ -64,16 +67,45 @@ export default class AccountSecurityController {
64
67
  return ctx.response.redirect('/account/security');
65
68
  }
66
69
  const { name, avatarUrl } = await ctx.request.validateUsing(updateProfileValidator);
70
+ // Upload de avatar via o drive do app (opt-in pela presença do arquivo).
71
+ // Se um arquivo for enviado e o drive estiver disponível, a URL resultante
72
+ // tem prioridade sobre o input de URL; senão caímos no avatarUrl (texto).
73
+ let resolvedAvatarUrl = avatarUrl ?? null;
74
+ let via = 'url';
75
+ const file = ctx.request.file('avatar', {
76
+ size: `${cfg.uploads.avatars.maxSizeMb}mb`,
77
+ extnames: ['jpg', 'jpeg', 'png', 'webp'],
78
+ });
79
+ if (file) {
80
+ try {
81
+ const uploadedUrl = await storeAvatar(ctx, cfg.uploads, file, userId, {
82
+ extname: translate(cfg.messages, 'account.profile.avatar_invalid_type'),
83
+ size: translate(cfg.messages, 'account.profile.avatar_too_large'),
84
+ });
85
+ if (uploadedUrl) {
86
+ resolvedAvatarUrl = uploadedUrl;
87
+ via = 'upload';
88
+ }
89
+ }
90
+ catch (error) {
91
+ if (error instanceof AvatarUploadError) {
92
+ ctx.session.flash('securityError', error.message);
93
+ return ctx.response.redirect('/account/security');
94
+ }
95
+ throw error;
96
+ }
97
+ }
67
98
  // Campos ausentes no form viram string vazia (limpa o valor); enviamos null
68
99
  // para limpar, ou o valor trimado.
69
100
  await store.updateProfile(userId, {
70
101
  name: name ?? null,
71
- avatarUrl: avatarUrl ?? null,
102
+ avatarUrl: resolvedAvatarUrl,
72
103
  });
73
104
  await cfg.audit?.record({
74
105
  type: 'profile.updated',
75
106
  accountId: userId,
76
107
  ip: ctx.request.ip?.() ?? null,
108
+ metadata: { via },
77
109
  });
78
110
  ctx.session.flash('profileUpdated', translate(cfg.messages, 'account.profile.updated'));
79
111
  return ctx.response.redirect('/account/security');
@@ -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 { sendPasswordResetEmail } from '../../default_mailer.js';
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 existing = await store.findByEmail(email);
65
- if (existing) {
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
- const account = await store.findById(accountId);
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
- if (supportsAccountStatus(store)) {
125
- if (disable)
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);
@@ -133,6 +133,10 @@ export declare const DEFAULT_MESSAGES: {
133
133
  'account.profile.intro': string;
134
134
  'account.profile.name_label': string;
135
135
  'account.profile.avatar_label': string;
136
+ 'account.profile.avatar_upload_label': string;
137
+ 'account.profile.avatar_upload_hint': string;
138
+ 'account.profile.avatar_invalid_type': string;
139
+ 'account.profile.avatar_too_large': string;
136
140
  'account.profile.submit': string;
137
141
  'account.profile.updated': string;
138
142
  'account.profile.not_supported': string;
@@ -432,6 +436,10 @@ export declare const PT_BR_MESSAGES: {
432
436
  'account.profile.intro': string;
433
437
  'account.profile.name_label': string;
434
438
  'account.profile.avatar_label': string;
439
+ 'account.profile.avatar_upload_label': string;
440
+ 'account.profile.avatar_upload_hint': string;
441
+ 'account.profile.avatar_invalid_type': string;
442
+ 'account.profile.avatar_too_large': string;
435
443
  'account.profile.submit': string;
436
444
  'account.profile.updated': string;
437
445
  'account.profile.not_supported': string;
@@ -135,6 +135,10 @@ export const DEFAULT_MESSAGES = {
135
135
  'account.profile.intro': 'Update your display name and avatar.',
136
136
  'account.profile.name_label': 'Name',
137
137
  'account.profile.avatar_label': 'Avatar URL',
138
+ 'account.profile.avatar_upload_label': 'Upload avatar',
139
+ 'account.profile.avatar_upload_hint': 'JPG, PNG or WebP, up to 5MB.',
140
+ 'account.profile.avatar_invalid_type': 'Invalid image type. Use JPG, PNG or WebP.',
141
+ 'account.profile.avatar_too_large': 'Image is too large.',
138
142
  'account.profile.submit': 'Save profile',
139
143
  'account.profile.updated': 'Profile updated successfully.',
140
144
  'account.profile.not_supported': 'Profile editing is not available in this installation.',
@@ -463,6 +467,10 @@ export const PT_BR_MESSAGES = {
463
467
  'account.profile.intro': 'Atualize seu nome de exibição e avatar.',
464
468
  'account.profile.name_label': 'Nome',
465
469
  'account.profile.avatar_label': 'URL do avatar',
470
+ 'account.profile.avatar_upload_label': 'Enviar avatar',
471
+ 'account.profile.avatar_upload_hint': 'JPG, PNG ou WebP, até 5MB.',
472
+ 'account.profile.avatar_invalid_type': 'Tipo de imagem inválido. Use JPG, PNG ou WebP.',
473
+ 'account.profile.avatar_too_large': 'A imagem é muito grande.',
466
474
  'account.profile.submit': 'Salvar perfil',
467
475
  'account.profile.updated': 'Perfil atualizado com sucesso.',
468
476
  'account.profile.not_supported': 'A edição de perfil não está disponível nesta instalação.',
@@ -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.6.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",
@@ -54,6 +54,7 @@
54
54
  "peerDependencies": {
55
55
  "@adonisjs/ally": "6.3.0",
56
56
  "@adonisjs/core": "7.3.3",
57
+ "@adonisjs/drive": "4.0.0",
57
58
  "@adonisjs/lucid": "22.4.2",
58
59
  "@adonisjs/session": "8.1.0",
59
60
  "@adonisjs/shield": "9.0.0",
@@ -65,6 +66,9 @@
65
66
  },
66
67
  "@adonisjs/ally": {
67
68
  "optional": true
69
+ },
70
+ "@adonisjs/drive": {
71
+ "optional": true
68
72
  }
69
73
  },
70
74
  "dependencies": {
@@ -81,6 +85,7 @@
81
85
  "devDependencies": {
82
86
  "@adonisjs/ally": "6.3.0",
83
87
  "@adonisjs/core": "7.3.3",
88
+ "@adonisjs/drive": "4.0.0",
84
89
  "@adonisjs/lucid": "22.4.2",
85
90
  "@adonisjs/session": "8.1.0",
86
91
  "@adonisjs/shield": "9.0.0",