@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
|
@@ -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:
|
|
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 {
|
|
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);
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -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;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -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.
|
|
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",
|