@adonis-agora/authkit-server 0.46.0 → 0.48.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/commands/clients_create.js +4 -4
- package/build/commands/configure.js +3 -2
- package/build/commands/doctor.js +8 -5
- package/build/commands/eject.js +1 -1
- package/build/commands/expire_scan.js +7 -5
- package/build/commands/import_users.js +1 -1
- package/build/commands/keys_rotate.js +7 -4
- package/build/host/views/account/apps.edge +5 -5
- package/build/host/views/account/login.edge +1 -1
- package/build/host/views/account/mfa.edge +7 -7
- package/build/host/views/account/orgs.edge +5 -5
- package/build/host/views/account/security.edge +10 -10
- package/build/host/views/account/tokens.edge +5 -5
- package/build/host/views/otp-unlock.edge +2 -2
- package/build/index.d.ts +7 -7
- package/build/index.js +5 -5
- package/build/providers/authkit_server_provider.d.ts +2 -2
- package/build/providers/authkit_server_provider.js +26 -10
- package/build/services/booted_app.d.ts +8 -0
- package/build/services/booted_app.js +27 -0
- package/build/services/main.d.ts +8 -1
- package/build/services/main.js +10 -2
- package/build/src/accounts/lucid_account_store.d.ts +3 -3
- package/build/src/accounts/lucid_account_store.js +6 -8
- package/build/src/accounts/lucid_store/core.js +1 -1
- package/build/src/accounts/lucid_store/mfa.js +1 -1
- package/build/src/accounts/lucid_store/organizations.js +2 -4
- package/build/src/accounts/lucid_store/password_hygiene.js +1 -4
- package/build/src/accounts/lucid_store/shared.d.ts +1 -1
- package/build/src/accounts/lucid_stores.d.ts +3 -3
- package/build/src/accounts/lucid_stores.js +2 -2
- package/build/src/adapters/database_adapter.js +3 -1
- package/build/src/adapters/factory.js +1 -1
- package/build/src/adapters/redis_adapter.js +2 -1
- package/build/src/commands/expire_scan_command.js +19 -5
- package/build/src/commands/settings_commands.js +22 -5
- package/build/src/define_config.d.ts +20 -20
- package/build/src/define_config.js +62 -67
- package/build/src/doctor/checks.js +45 -20
- package/build/src/events/dispatcher.d.ts +1 -1
- package/build/src/events/dispatcher.js +10 -10
- package/build/src/host/account_api/account_api_controller.js +95 -39
- package/build/src/host/account_deletion_ops.d.ts +4 -4
- package/build/src/host/account_deletion_ops.js +5 -5
- package/build/src/host/account_deletion_service.d.ts +2 -2
- package/build/src/host/account_deletion_service.js +2 -2
- package/build/src/host/account_export_service.d.ts +1 -1
- package/build/src/host/account_export_service.js +4 -6
- package/build/src/host/account_home.d.ts +0 -8
- package/build/src/host/account_home.js +6 -4
- package/build/src/host/account_login_url.d.ts +44 -0
- package/build/src/host/account_login_url.js +58 -0
- package/build/src/host/account_paths.d.ts +120 -0
- package/build/src/host/account_paths.js +161 -0
- package/build/src/host/admin_api/admin_orgs_service.d.ts +2 -2
- package/build/src/host/admin_api/admin_orgs_service.js +3 -2
- package/build/src/host/admin_api/admin_users_service.d.ts +12 -12
- package/build/src/host/admin_api/admin_users_service.js +20 -20
- package/build/src/host/admin_api/api_clients_controller.js +2 -2
- package/build/src/host/admin_api/api_misc_controller.js +2 -2
- package/build/src/host/admin_api/api_orgs_controller.js +3 -3
- package/build/src/host/admin_api/api_settings_controller.js +11 -5
- package/build/src/host/admin_api/api_users_controller.d.ts +2 -2
- package/build/src/host/admin_api/api_users_controller.js +37 -38
- package/build/src/host/admin_api/dto.d.ts +4 -4
- package/build/src/host/admin_clients_service.js +5 -4
- package/build/src/host/admin_console/admin_shell_controller.js +4 -2
- package/build/src/host/admin_console/console_audit_controller.js +1 -1
- package/build/src/host/admin_console/console_clients_controller.js +5 -3
- package/build/src/host/admin_console/console_impersonation_controller.js +1 -1
- package/build/src/host/admin_console/console_keys_controller.js +2 -4
- package/build/src/host/admin_console/console_orgs_controller.js +3 -3
- package/build/src/host/admin_console/console_roles_controller.js +30 -12
- package/build/src/host/admin_console/console_sessions_controller.js +3 -3
- package/build/src/host/admin_console/console_settings_controller.js +11 -5
- package/build/src/host/admin_console/console_users_controller.d.ts +2 -2
- package/build/src/host/admin_console/console_users_controller.js +46 -47
- package/build/src/host/admin_prefix.js +2 -2
- package/build/src/host/admin_sessions_service.d.ts +3 -1
- package/build/src/host/admin_sessions_service.js +36 -6
- package/build/src/host/adonis_auth_sync.d.ts +1 -1
- package/build/src/host/adonis_auth_user_provider.d.ts +1 -1
- package/build/src/host/app_key.js +1 -1
- package/build/src/host/avatar_storage.js +1 -3
- package/build/src/host/config_locks.js +1 -3
- package/build/src/host/console_session.d.ts +4 -0
- package/build/src/host/console_session.js +9 -2
- package/build/src/host/controllers/account_apps_controller.js +3 -2
- package/build/src/host/controllers/account_confirm_controller.js +2 -6
- package/build/src/host/controllers/account_mfa_controller.js +63 -12
- package/build/src/host/controllers/account_orgs_controller.js +53 -30
- package/build/src/host/controllers/account_security_controller.d.ts +2 -2
- package/build/src/host/controllers/account_security_controller.js +116 -116
- package/build/src/host/controllers/account_session_controller.js +7 -5
- package/build/src/host/controllers/account_tokens_controller.js +14 -2
- package/build/src/host/controllers/interaction_controller.js +59 -22
- package/build/src/host/controllers/pat_introspection_controller.js +10 -0
- package/build/src/host/controllers/registration_controller.js +28 -13
- package/build/src/host/default_mailer.js +11 -7
- package/build/src/host/durable/account_deletion_workflow.d.ts +2 -2
- package/build/src/host/durable/account_deletion_workflow.js +14 -14
- package/build/src/host/durable/account_export_workflow.d.ts +2 -2
- package/build/src/host/durable/account_export_workflow.js +12 -12
- package/build/src/host/durable/index.d.ts +5 -5
- package/build/src/host/durable/index.js +6 -6
- package/build/src/host/i18n.d.ts +1376 -1376
- package/build/src/host/i18n.js +1380 -1380
- package/build/src/host/impersonation.js +1 -7
- package/build/src/host/key_rotation_actions.d.ts +1 -1
- package/build/src/host/login_attempt.js +35 -6
- package/build/src/host/login_notify.js +1 -1
- package/build/src/host/middleware/account_auth.js +3 -1
- package/build/src/host/otp_lockout.js +1 -3
- package/build/src/host/rate_limit.js +1 -1
- package/build/src/host/register_auth_host.d.ts +96 -2
- package/build/src/host/register_auth_host.js +196 -91
- package/build/src/host/renderers/edge_renderer.js +19 -1
- package/build/src/host/renderers/inertia_renderer.d.ts +63 -0
- package/build/src/host/renderers/inertia_renderer.js +63 -0
- package/build/src/host/runtime_settings.js +1 -1
- package/build/src/host/runtime_toggles.d.ts +3 -3
- package/build/src/host/runtime_toggles.js +69 -24
- package/build/src/host/security_notice_service.d.ts +1 -1
- package/build/src/host/security_notice_service.js +1 -1
- package/build/src/host/session_context.js +1 -1
- package/build/src/host/sudo/index.d.ts +3 -3
- package/build/src/host/sudo/index.js +3 -3
- package/build/src/host/sudo/methods/magic_link.js +6 -5
- package/build/src/host/sudo/methods/passkey.js +7 -4
- package/build/src/host/sudo/methods/password.js +8 -3
- package/build/src/host/sudo/runtime.js +6 -13
- package/build/src/host/sudo_mode.js +3 -2
- package/build/src/host/ui-dist/assets/index-BQoTDiLV.js +137 -0
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/keys/jwks_manager.js +1 -1
- package/build/src/keys/keystore.js +1 -1
- package/build/src/keys/keystore_manager.d.ts +2 -2
- package/build/src/keys/keystore_manager.js +2 -2
- package/build/src/keys/keystore_vault.js +8 -4
- package/build/src/mixins/with_audit_log.d.ts +1 -1
- package/build/src/mixins/with_auth_user.d.ts +1 -1
- package/build/src/mixins/with_auth_user.js +1 -1
- package/build/src/mixins/with_credentials.d.ts +1 -1
- package/build/src/mixins/with_mfa.d.ts +1 -1
- package/build/src/mixins/with_personal_access_token.d.ts +1 -1
- package/build/src/mixins/with_provider_identity.d.ts +1 -1
- package/build/src/mixins/with_webauthn_credential.d.ts +1 -1
- package/build/src/observability/diagnostics_bridge.js +2 -2
- package/build/src/observability/metrics_service.js +1 -1
- package/build/src/observability/telescope/data_providers.d.ts +1 -1
- package/build/src/observability/telescope/data_providers.js +29 -43
- package/build/src/observability/telescope/extension.d.ts +1 -1
- package/build/src/observability/telescope/extension.js +58 -58
- package/build/src/observability/telescope/index.d.ts +2 -2
- package/build/src/observability/telescope/index.js +2 -2
- package/build/src/observability/wire_provider_events.d.ts +1 -1
- package/build/src/observability/wire_provider_events.js +11 -11
- package/build/src/password/common_passwords.js +1 -1
- package/build/src/password/password_manager.d.ts +1 -1
- package/build/src/password/password_manager.js +4 -4
- package/build/src/pat/lucid_pat_store.js +2 -6
- package/build/src/pat/pat_tokens.js +1 -1
- package/build/src/provider/interaction_actions.js +1 -1
- package/build/src/provider/key_rotation_scheduler.js +3 -1
- package/build/src/provider/logout_sources.js +2 -8
- package/build/src/provider/oidc_service.d.ts +2 -2
- package/build/src/provider/oidc_service.js +22 -14
- package/build/src/provider/single_flight_lock.js +1 -3
- package/build/src/provider/token_exchange.d.ts +1 -1
- package/build/src/schema/ensure.js +3 -1
- package/build/stubs/ui/react/components/auth_shell.tsx +19 -19
- package/build/stubs/ui/react/pages/account/login.tsx +6 -7
- package/build/stubs/ui/react/pages/account/mfa.tsx +16 -15
- package/build/stubs/ui/react/pages/account/tokens.tsx +27 -17
- package/build/stubs/ui/react/pages/consent.tsx +12 -12
- package/build/stubs/ui/react/pages/forgot.tsx +7 -7
- package/build/stubs/ui/react/pages/login.tsx +22 -24
- package/build/stubs/ui/react/pages/mfa-challenge.tsx +9 -10
- package/build/stubs/ui/react/pages/reset.tsx +10 -10
- package/build/stubs/ui/react/pages/signup.tsx +30 -16
- package/build/stubs/ui/react/pages/verify-email.tsx +6 -6
- package/package.json +1 -1
- package/stubs/main.ts +2 -2
- package/stubs/ui/react/components/auth_shell.tsx +19 -19
- package/stubs/ui/react/pages/account/login.tsx +6 -7
- package/stubs/ui/react/pages/account/mfa.tsx +16 -15
- package/stubs/ui/react/pages/account/tokens.tsx +27 -17
- package/stubs/ui/react/pages/consent.tsx +12 -12
- package/stubs/ui/react/pages/forgot.tsx +7 -7
- package/stubs/ui/react/pages/login.tsx +22 -24
- package/stubs/ui/react/pages/mfa-challenge.tsx +9 -10
- package/stubs/ui/react/pages/reset.tsx +10 -10
- package/stubs/ui/react/pages/signup.tsx +30 -16
- package/stubs/ui/react/pages/verify-email.tsx +6 -6
- package/build/src/host/ui-dist/assets/index-BmI_J87g.js +0 -137
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { supportsAccountDeletion, supportsAccountStatus } from '../../accounts/account_store.js';
|
|
3
|
+
import { AdminUsersService } from '../admin_api/admin_users_service.js';
|
|
4
|
+
import { apiError, grantDto, sessionDto, userDto } from '../admin_api/dto.js';
|
|
5
|
+
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
6
|
+
import { adminUserCreateValidator, adminUserRolesValidator } from '../admin_validators.js';
|
|
7
|
+
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
8
|
+
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
9
|
+
import { resolveEffectiveRolesCatalog } from '../runtime_toggles.js';
|
|
10
|
+
import { enrichSessionsWithContext } from '../session_context.js';
|
|
11
11
|
const PAGE_SIZE = 20;
|
|
12
12
|
/**
|
|
13
13
|
* Endpoints JSON de usuários do console admin React.
|
|
@@ -28,12 +28,11 @@ const PAGE_SIZE = 20;
|
|
|
28
28
|
export default class ConsoleUsersController {
|
|
29
29
|
/** GET {prefix}/api/users */
|
|
30
30
|
async index(ctx) {
|
|
31
|
-
const service = await ctx.containerResolver.make(
|
|
31
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
32
32
|
const cfg = service.config;
|
|
33
|
-
const search = ctx.request.input(
|
|
34
|
-
const page = Math.max(1, Number.parseInt(ctx.request.input(
|
|
35
|
-
const perPage = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input(
|
|
36
|
-
PAGE_SIZE));
|
|
33
|
+
const search = ctx.request.input('search', '').trim();
|
|
34
|
+
const page = Math.max(1, Number.parseInt(ctx.request.input('page', '1'), 10) || 1);
|
|
35
|
+
const perPage = Math.max(1, Math.min(100, Number.parseInt(ctx.request.input('perPage', String(PAGE_SIZE)), 10) || PAGE_SIZE));
|
|
37
36
|
const result = await cfg.accountStore.listAccounts({
|
|
38
37
|
search,
|
|
39
38
|
page,
|
|
@@ -45,12 +44,12 @@ export default class ConsoleUsersController {
|
|
|
45
44
|
}
|
|
46
45
|
/** GET {prefix}/api/users/:id */
|
|
47
46
|
async show(ctx) {
|
|
48
|
-
const service = await ctx.containerResolver.make(
|
|
47
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
49
48
|
const cfg = service.config;
|
|
50
|
-
const id = ctx.request.param(
|
|
49
|
+
const id = ctx.request.param('id');
|
|
51
50
|
const account = await cfg.accountStore.findById(id);
|
|
52
51
|
if (!account)
|
|
53
|
-
return ctx.response.notFound(apiError(
|
|
52
|
+
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
54
53
|
const users = new AdminUsersService(cfg);
|
|
55
54
|
const disabled = await users.isDisabled(id);
|
|
56
55
|
// Sessões ativas (degradam quando adapter não enumera).
|
|
@@ -82,7 +81,7 @@ export default class ConsoleUsersController {
|
|
|
82
81
|
}
|
|
83
82
|
/** POST {prefix}/api/users */
|
|
84
83
|
async store(ctx) {
|
|
85
|
-
const service = await ctx.containerResolver.make(
|
|
84
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
86
85
|
const cfg = service.config;
|
|
87
86
|
const actorId = ctx.session?.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
88
87
|
const ip = ctx.request.ip?.() ?? null;
|
|
@@ -93,45 +92,45 @@ export default class ConsoleUsersController {
|
|
|
93
92
|
name: name ?? null,
|
|
94
93
|
password: password ?? null,
|
|
95
94
|
invite: invite ?? false,
|
|
96
|
-
}, { actorId, ip, source:
|
|
95
|
+
}, { actorId, ip, source: 'admin' });
|
|
97
96
|
if (!result.ok) {
|
|
98
|
-
if (result.reason ===
|
|
99
|
-
return ctx.response.badRequest(apiError(
|
|
97
|
+
if (result.reason === 'password_policy') {
|
|
98
|
+
return ctx.response.badRequest(apiError('password_policy', cfg.messages[result.messageKey] ?? result.messageKey));
|
|
100
99
|
}
|
|
101
|
-
return ctx.response.conflict(apiError(
|
|
100
|
+
return ctx.response.conflict(apiError('email_taken', 'Já existe uma conta com este e-mail.'));
|
|
102
101
|
}
|
|
103
102
|
ctx.response.status(201);
|
|
104
103
|
return { ...userDto(result.account), invited: result.invited };
|
|
105
104
|
}
|
|
106
105
|
/** PATCH {prefix}/api/users/:id/roles */
|
|
107
106
|
async updateRoles(ctx) {
|
|
108
|
-
const service = await ctx.containerResolver.make(
|
|
107
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
109
108
|
const cfg = service.config;
|
|
110
|
-
const id = ctx.request.param(
|
|
109
|
+
const id = ctx.request.param('id');
|
|
111
110
|
const account = await cfg.accountStore.findById(id);
|
|
112
111
|
if (!account)
|
|
113
|
-
return ctx.response.notFound(apiError(
|
|
112
|
+
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
114
113
|
const actorId = ctx.session?.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
115
114
|
const { roles: rolesInput } = await ctx.request.validateUsing(adminUserRolesValidator);
|
|
116
115
|
const roles = Array.from(new Set((rolesInput ?? []).map((r) => r.trim()).filter(Boolean)));
|
|
117
116
|
const users = new AdminUsersService(cfg);
|
|
118
117
|
// Proteções de escalonamento/lockout: último admin + auto-rebaixamento.
|
|
119
118
|
const guard = await users.guardGlobalRolesChange(id, roles, actorId);
|
|
120
|
-
if (guard ===
|
|
119
|
+
if (guard === 'last_admin') {
|
|
121
120
|
return ctx.response
|
|
122
121
|
.status(409)
|
|
123
|
-
.send(apiError(
|
|
122
|
+
.send(apiError('last_admin', 'Não é possível remover a última conta com a role de administrador.'));
|
|
124
123
|
}
|
|
125
|
-
if (guard ===
|
|
124
|
+
if (guard === 'cannot_self_demote') {
|
|
126
125
|
return ctx.response
|
|
127
126
|
.status(409)
|
|
128
|
-
.send(apiError(
|
|
127
|
+
.send(apiError('cannot_self_demote', 'Você não pode remover a sua própria role de administrador.'));
|
|
129
128
|
}
|
|
130
129
|
// Resolve RuntimeSettings para validação contra catálogo (fail-safe).
|
|
131
130
|
const runtimeSettings = await resolveRuntimeSettings(ctx);
|
|
132
131
|
const errorKey = await users.setGlobalRolesValidated(id, roles, runtimeSettings);
|
|
133
132
|
if (errorKey) {
|
|
134
|
-
return ctx.response.badRequest(apiError(
|
|
133
|
+
return ctx.response.badRequest(apiError('invalid_role', cfg.messages[errorKey] ?? errorKey));
|
|
135
134
|
}
|
|
136
135
|
const updated = await cfg.accountStore.findById(id);
|
|
137
136
|
const disabled = await users.isDisabled(id);
|
|
@@ -146,24 +145,24 @@ export default class ConsoleUsersController {
|
|
|
146
145
|
return this.#setStatus(ctx, false);
|
|
147
146
|
}
|
|
148
147
|
async #setStatus(ctx, disable) {
|
|
149
|
-
const service = await ctx.containerResolver.make(
|
|
148
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
150
149
|
const cfg = service.config;
|
|
151
150
|
const actorId = ctx.session?.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
152
151
|
const ip = ctx.request.ip?.() ?? null;
|
|
153
|
-
const id = ctx.request.param(
|
|
152
|
+
const id = ctx.request.param('id');
|
|
154
153
|
const account = await cfg.accountStore.findById(id);
|
|
155
154
|
if (!account)
|
|
156
|
-
return ctx.response.notFound(apiError(
|
|
155
|
+
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
157
156
|
const users = new AdminUsersService(cfg);
|
|
158
157
|
const applied = await users.setStatus(id, disable, {
|
|
159
158
|
actorId,
|
|
160
159
|
ip,
|
|
161
|
-
source:
|
|
160
|
+
source: 'admin',
|
|
162
161
|
});
|
|
163
162
|
if (!applied) {
|
|
164
163
|
return ctx.response
|
|
165
164
|
.status(409)
|
|
166
|
-
.send(apiError(
|
|
165
|
+
.send(apiError('capability_unsupported', 'O store não suporta habilitar/desabilitar contas.'));
|
|
167
166
|
}
|
|
168
167
|
const updated = await cfg.accountStore.findById(id);
|
|
169
168
|
const disabled = await users.isDisabled(id);
|
|
@@ -171,41 +170,41 @@ export default class ConsoleUsersController {
|
|
|
171
170
|
}
|
|
172
171
|
/** POST {prefix}/api/users/:id/reset-password */
|
|
173
172
|
async resetPassword(ctx) {
|
|
174
|
-
const service = await ctx.containerResolver.make(
|
|
173
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
175
174
|
const cfg = service.config;
|
|
176
175
|
const actorId = ctx.session?.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
177
176
|
const ip = ctx.request.ip?.() ?? null;
|
|
178
|
-
const id = ctx.request.param(
|
|
177
|
+
const id = ctx.request.param('id');
|
|
179
178
|
const users = new AdminUsersService(cfg);
|
|
180
179
|
const account = await users.resetPassword(ctx, id, {
|
|
181
180
|
actorId,
|
|
182
181
|
ip,
|
|
183
|
-
source:
|
|
182
|
+
source: 'admin',
|
|
184
183
|
});
|
|
185
184
|
if (!account)
|
|
186
|
-
return ctx.response.notFound(apiError(
|
|
185
|
+
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
187
186
|
return { ok: true, email: account.email };
|
|
188
187
|
}
|
|
189
188
|
/** DELETE {prefix}/api/users/:id */
|
|
190
189
|
async destroy(ctx) {
|
|
191
|
-
const service = await ctx.containerResolver.make(
|
|
190
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
192
191
|
const cfg = service.config;
|
|
193
192
|
const actorId = ctx.session?.get(ACCOUNT_SESSION_KEY) ?? null;
|
|
194
193
|
const ip = ctx.request.ip?.() ?? null;
|
|
195
|
-
const id = ctx.request.param(
|
|
194
|
+
const id = ctx.request.param('id');
|
|
196
195
|
const users = new AdminUsersService(cfg);
|
|
197
196
|
// OPT-IN durável: passa o enqueue (cascade async); senão delete() roda síncrono.
|
|
198
197
|
const enqueue = cfg.accountLifecycle?.durable
|
|
199
|
-
? (await import(
|
|
198
|
+
? (await import('../durable/index.js')).enqueueDeletionVia(ctx.containerResolver)
|
|
200
199
|
: undefined;
|
|
201
|
-
const outcome = await users.delete(service, id, { actorId, ip, source:
|
|
200
|
+
const outcome = await users.delete(service, id, { actorId, ip, source: 'admin' }, enqueue);
|
|
202
201
|
if (!outcome.ok) {
|
|
203
|
-
if (outcome.reason ===
|
|
204
|
-
return ctx.response.notFound(apiError(
|
|
202
|
+
if (outcome.reason === 'not_found') {
|
|
203
|
+
return ctx.response.notFound(apiError('not_found', 'Usuário não encontrado.'));
|
|
205
204
|
}
|
|
206
205
|
return ctx.response
|
|
207
206
|
.status(409)
|
|
208
|
-
.send(apiError(
|
|
207
|
+
.send(apiError('capability_unsupported', 'O store não suporta deleção de contas.'));
|
|
209
208
|
}
|
|
210
209
|
return { ok: true, deleted: id };
|
|
211
210
|
}
|
|
@@ -22,7 +22,7 @@ let _prefix = DEFAULT_ADMIN_PREFIX;
|
|
|
22
22
|
export function normalizeAdminPrefix(raw) {
|
|
23
23
|
let p = raw.trim();
|
|
24
24
|
if (!p.startsWith('/'))
|
|
25
|
-
p =
|
|
25
|
+
p = `/${p}`;
|
|
26
26
|
p = p.replace(/\/+$/, '');
|
|
27
27
|
return p || '/';
|
|
28
28
|
}
|
|
@@ -53,7 +53,7 @@ let _apiPrefix = DEFAULT_ADMIN_API_PREFIX;
|
|
|
53
53
|
export function normalizeAdminApiPrefix(raw) {
|
|
54
54
|
let p = raw.trim();
|
|
55
55
|
if (!p.startsWith('/'))
|
|
56
|
-
p =
|
|
56
|
+
p = `/${p}`;
|
|
57
57
|
p = p.replace(/\/+$/, '');
|
|
58
58
|
return p || '/';
|
|
59
59
|
}
|
|
@@ -55,7 +55,9 @@ export declare class AdminSessionsService {
|
|
|
55
55
|
* a sessão na próxima request — INSTANTÂNEO, sem esperar o refresh token falhar.
|
|
56
56
|
*
|
|
57
57
|
* Best-effort: a destruição de grants/tokens acima já é a fonte da verdade; esta
|
|
58
|
-
* linha só acelera o efeito nos clients. Falha (tabela ausente
|
|
58
|
+
* linha só acelera o efeito nos clients. Falha (tabela ausente / lucid indisponível)
|
|
59
|
+
* NÃO propaga — mas NUNCA é silenciosa: é logada em `error`, porque uma revogação de
|
|
60
|
+
* sessão que não persiste é um risco de segurança que não pode ficar invisível.
|
|
59
61
|
*
|
|
60
62
|
* @param accountId Conta cujas sessões devem ser derrubadas (gravado em `sub`).
|
|
61
63
|
* @param revokedAt Timestamp da revogação. Default: agora. A checagem do client
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getBootedApp } from '../../services/booted_app.js';
|
|
1
2
|
/**
|
|
2
3
|
* Serviço de inspeção/revogação das SESSÕES e GRANTS ativos de uma conta,
|
|
3
4
|
* persistidos pelo oidc-provider via o MESMO `AdapterClass` (mesmo padrão do
|
|
@@ -32,7 +33,9 @@ export class AdminSessionsService {
|
|
|
32
33
|
* a sessão na próxima request — INSTANTÂNEO, sem esperar o refresh token falhar.
|
|
33
34
|
*
|
|
34
35
|
* Best-effort: a destruição de grants/tokens acima já é a fonte da verdade; esta
|
|
35
|
-
* linha só acelera o efeito nos clients. Falha (tabela ausente
|
|
36
|
+
* linha só acelera o efeito nos clients. Falha (tabela ausente / lucid indisponível)
|
|
37
|
+
* NÃO propaga — mas NUNCA é silenciosa: é logada em `error`, porque uma revogação de
|
|
38
|
+
* sessão que não persiste é um risco de segurança que não pode ficar invisível.
|
|
36
39
|
*
|
|
37
40
|
* @param accountId Conta cujas sessões devem ser derrubadas (gravado em `sub`).
|
|
38
41
|
* @param revokedAt Timestamp da revogação. Default: agora. A checagem do client
|
|
@@ -43,7 +46,16 @@ export class AdminSessionsService {
|
|
|
43
46
|
*/
|
|
44
47
|
async recordSubRevocation(accountId, revokedAt = new Date()) {
|
|
45
48
|
try {
|
|
46
|
-
|
|
49
|
+
// Resolve o Lucid pelo ALIAS STRING `'lucid.db'` (o mesmo idioma de
|
|
50
|
+
// `runtime_settings.ts` e do provider), NUNCA por `import('@adonisjs/lucid/services/db')`
|
|
51
|
+
// — esse resolve a CLASSE `Database` e a usa como TOKEN do container. Com duas cópias
|
|
52
|
+
// físicas do lucid na árvore do host (pins distintos ou o mesmo pin sob peer sets
|
|
53
|
+
// distintos, que o pnpm materializa em diretórios separados) os tokens-classe diferem, o
|
|
54
|
+
// `make()` falha (`Cannot construct "[class Database]"`) e a gravação era engolida em
|
|
55
|
+
// SILÊNCIO por este catch — a pior variante do bug. Um token string não pode ser
|
|
56
|
+
// duplicado. O app vem do provider (ver `services/booted_app.ts`), não de `services/app`.
|
|
57
|
+
const app = getBootedApp();
|
|
58
|
+
const db = await app.container.make('lucid.db');
|
|
47
59
|
const conn = this.#schemaConnection
|
|
48
60
|
? db.connection(this.#schemaConnection)
|
|
49
61
|
: db.connection();
|
|
@@ -53,8 +65,28 @@ export class AdminSessionsService {
|
|
|
53
65
|
revoked_at: revokedAt,
|
|
54
66
|
});
|
|
55
67
|
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// A invalidação server-side (destruição de grants/tokens) já é a fonte da verdade, então
|
|
70
|
+
// NÃO propagamos. Mas NUNCA engolimos em silêncio: logamos em `error` para que a falha da
|
|
71
|
+
// propagação cookie-based (tabela ausente, lucid indisponível, provider não registrado)
|
|
72
|
+
// seja visível — uma revogação que não persiste é um risco de segurança invisível.
|
|
73
|
+
await this.#logRevocationFailure(accountId, error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Loga (best-effort, sem nunca lançar) a falha de {@link recordSubRevocation}. Prefere o
|
|
78
|
+
* `logger` do container; se ele não puder ser resolvido (ex.: o próprio `getBootedApp()` foi o
|
|
79
|
+
* que falhou), cai em `console.error` — a falha PRECISA aparecer em algum lugar.
|
|
80
|
+
*/
|
|
81
|
+
async #logRevocationFailure(accountId, error) {
|
|
82
|
+
const msg = 'authkit: falha ao gravar a revogação por sub (auth_session_revocations) — a propagação cookie-based da revogação de sessão não foi persistida';
|
|
83
|
+
try {
|
|
84
|
+
const logger = await getBootedApp().container.make('logger');
|
|
85
|
+
logger.error({ err: error, accountId }, msg);
|
|
86
|
+
}
|
|
56
87
|
catch {
|
|
57
|
-
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.error(msg, { accountId, error });
|
|
58
90
|
}
|
|
59
91
|
}
|
|
60
92
|
/** Indica se o adapter suporta enumeração (capacidade opcional). */
|
|
@@ -198,9 +230,7 @@ export class AdminSessionsService {
|
|
|
198
230
|
//
|
|
199
231
|
// Sem sessão preservada (exceptSessionId não encontrado / sem loginTs), caímos
|
|
200
232
|
// no default (`now`) — não há sessão nova a proteger nesse caminho.
|
|
201
|
-
const cutoff = typeof preserved?.loginTs === 'number'
|
|
202
|
-
? new Date((preserved.loginTs - 1) * 1000)
|
|
203
|
-
: undefined;
|
|
233
|
+
const cutoff = typeof preserved?.loginTs === 'number' ? new Date((preserved.loginTs - 1) * 1000) : undefined;
|
|
204
234
|
await this.recordSubRevocation(accountId, cutoff);
|
|
205
235
|
return {
|
|
206
236
|
sessions: sessionsToRevoke.length,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
-
import type { ResolvedServerConfig } from '../define_config.js';
|
|
3
2
|
import type { AuthAccount } from '../accounts/account_store.js';
|
|
3
|
+
import type { ResolvedServerConfig } from '../define_config.js';
|
|
4
4
|
/**
|
|
5
5
|
* Sincroniza `ctx.auth` (`@adonisjs/auth`) com um login bem-sucedido no console
|
|
6
6
|
* de conta do authkit. Ver `adonisAuth` em `define_config.ts` pro porquê: sem
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
2
1
|
import type { SessionUserProviderContract } from '@adonisjs/auth/types/session';
|
|
2
|
+
import type { ConfigProvider } from '@adonisjs/core/types';
|
|
3
3
|
import type { AuthAccount } from '../accounts/account_store.js';
|
|
4
4
|
/**
|
|
5
5
|
* User provider de `@adonisjs/auth` (session guard) apoiado no `accountStore`
|
|
@@ -4,7 +4,7 @@ import { RuntimeException } from '@adonisjs/core/exceptions';
|
|
|
4
4
|
* LAZY do `register()`) para o erro de `appKey` ausente — um só lugar para
|
|
5
5
|
* editar o texto, e garante que os dois caminhos falem exatamente a mesma coisa.
|
|
6
6
|
*/
|
|
7
|
-
export const MISSING_APP_KEY_MESSAGE =
|
|
7
|
+
export const MISSING_APP_KEY_MESSAGE = "APP_KEY ausente: defina `export const appKey = new Secret(env.get('APP_KEY'))` em config/app.ts. " +
|
|
8
8
|
'O @adonis-agora/authkit-server precisa dele para assinar os cookies do oidc-provider (NÃO usa config/encryption.ts).';
|
|
9
9
|
/**
|
|
10
10
|
* Lê e valida `app.appKey` (usado pelo oidc-provider para assinar cookies via
|
|
@@ -43,9 +43,7 @@ let mediaModulePromise;
|
|
|
43
43
|
async function loadMedia() {
|
|
44
44
|
if (!mediaModulePromise) {
|
|
45
45
|
const specifier = '@adonis-agora/media/single-file';
|
|
46
|
-
mediaModulePromise = import(__rewriteRelativeImportExtension(specifier))
|
|
47
|
-
.then((mod) => mod ?? null)
|
|
48
|
-
.catch(() => null);
|
|
46
|
+
mediaModulePromise = import(__rewriteRelativeImportExtension(specifier)).then((mod) => mod ?? null).catch(() => null);
|
|
49
47
|
}
|
|
50
48
|
return mediaModulePromise;
|
|
51
49
|
}
|
|
@@ -48,9 +48,7 @@ export class SettingLockedError extends Error {
|
|
|
48
48
|
code = 'E_SETTING_LOCKED';
|
|
49
49
|
key;
|
|
50
50
|
constructor(key) {
|
|
51
|
-
super(`A setting "${key}" foi definida via defineConfig() e está travada —
|
|
52
|
-
`não pode ser alterada em runtime (console/Admin API). ` +
|
|
53
|
-
`Remova-a do defineConfig para liberar a edição pela UI.`);
|
|
51
|
+
super(`A setting "${key}" foi definida via defineConfig() e está travada — não pode ser alterada em runtime (console/Admin API). Remova-a do defineConfig para liberar a edição pela UI.`);
|
|
54
52
|
this.name = 'SettingLockedError';
|
|
55
53
|
this.key = key;
|
|
56
54
|
}
|
|
@@ -21,6 +21,10 @@ export declare function hasAccountSession(ctx: HttpContext): boolean;
|
|
|
21
21
|
* URL do login do console, com `return_to` opcional de volta ao destino
|
|
22
22
|
* original após autenticar.
|
|
23
23
|
*
|
|
24
|
+
* Respeita a opção `accountLoginUrl` de `registerAuthHost`: quando o host
|
|
25
|
+
* desmontou a tela `account/login` e apontou para a própria rota de login
|
|
26
|
+
* (ex.: `/login`), este helper usa esse destino. Default `/account/login`.
|
|
27
|
+
*
|
|
24
28
|
* @example
|
|
25
29
|
* consoleLoginUrl('/telescope') // => '/account/login?return_to=%2Ftelescope'
|
|
26
30
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAccountLoginUrl } from './account_login_url.js';
|
|
1
2
|
import { ACCOUNT_SESSION_KEY } from './middleware/account_auth.js';
|
|
2
3
|
/**
|
|
3
4
|
* Helpers públicos para integrar a sessão do console do AuthKit com
|
|
@@ -26,11 +27,17 @@ export function hasAccountSession(ctx) {
|
|
|
26
27
|
* URL do login do console, com `return_to` opcional de volta ao destino
|
|
27
28
|
* original após autenticar.
|
|
28
29
|
*
|
|
30
|
+
* Respeita a opção `accountLoginUrl` de `registerAuthHost`: quando o host
|
|
31
|
+
* desmontou a tela `account/login` e apontou para a própria rota de login
|
|
32
|
+
* (ex.: `/login`), este helper usa esse destino. Default `/account/login`.
|
|
33
|
+
*
|
|
29
34
|
* @example
|
|
30
35
|
* consoleLoginUrl('/telescope') // => '/account/login?return_to=%2Ftelescope'
|
|
31
36
|
*/
|
|
32
37
|
export function consoleLoginUrl(returnTo) {
|
|
38
|
+
const loginUrl = getAccountLoginUrl();
|
|
33
39
|
if (!returnTo)
|
|
34
|
-
return
|
|
35
|
-
|
|
40
|
+
return loginUrl;
|
|
41
|
+
const sep = loginUrl.includes('?') ? '&' : '?';
|
|
42
|
+
return `${loginUrl}${sep}return_to=${encodeURIComponent(returnTo)}`;
|
|
36
43
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
|
-
import {
|
|
2
|
+
import { accountPath } from '../account_paths.js';
|
|
3
3
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
4
|
+
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
4
5
|
/**
|
|
5
6
|
* Self-service de consentimento ("apps com acesso") no console de conta. Lista os
|
|
6
7
|
* Grants da própria conta agrupados por client (resolvendo o nome do client da
|
|
@@ -56,6 +57,6 @@ export default class AccountAppsController {
|
|
|
56
57
|
},
|
|
57
58
|
});
|
|
58
59
|
ctx.session.flash('appRevoked', cfg.messages['account.apps.revoked'] ?? 'account.apps.revoked');
|
|
59
|
-
return ctx.response.redirect('
|
|
60
|
+
return ctx.response.redirect(accountPath('apps'));
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* A tela está atrás do `accountGuard` (requer sessão de conta ativa).
|
|
9
9
|
*/
|
|
10
10
|
import '../augmentations.js';
|
|
11
|
-
import {
|
|
11
|
+
import { LAST_METHOD_SESSION_KEY, configuredSudoMethods, isSudoMethodMounted, resolveAvailableMethods, sudoContextFrom, } from '../sudo/runtime.js';
|
|
12
12
|
/**
|
|
13
13
|
* Reexport de compatibilidade. O construtor canônico do `SudoContext` vive em
|
|
14
14
|
* `sudo/runtime.ts` (é runtime do SPI, não detalhe da tela); este caminho
|
|
@@ -23,7 +23,6 @@ export default class AccountConfirmController {
|
|
|
23
23
|
if (!methods.length) {
|
|
24
24
|
// Nenhum método disponível é erro de CONFIGURAÇÃO do host, não usuário
|
|
25
25
|
// preso: a tela informa e o log aponta o problema.
|
|
26
|
-
;
|
|
27
26
|
ctx.logger?.error({ accountId: c.accountId }, 'authkit: nenhum método de sudo disponível para a conta — verifique config.sudo.methods');
|
|
28
27
|
}
|
|
29
28
|
// FLAG-DRIFT: `config.sudo.methods` decide o que a tela OFERECE, mas as
|
|
@@ -35,10 +34,7 @@ export default class AccountConfirmController {
|
|
|
35
34
|
// `configuredSudoMethods` devolve a própria lista montada.
|
|
36
35
|
for (const m of methods) {
|
|
37
36
|
if (!isSudoMethodMounted(m.id)) {
|
|
38
|
-
;
|
|
39
|
-
ctx.logger?.warn({ method: m.id }, `authkit: método de sudo "${m.id}" está em config.sudo.methods mas não teve ` +
|
|
40
|
-
'rotas montadas por registerAuthHost — a tela vai oferecer uma opção cujo ' +
|
|
41
|
-
'endpoint não existe');
|
|
37
|
+
ctx.logger?.warn({ method: m.id }, `authkit: método de sudo "${m.id}" está em config.sudo.methods mas não teve rotas montadas por registerAuthHost — a tela vai oferecer uma opção cujo endpoint não existe`);
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
return c.cfg.render(ctx, 'account/confirm', {
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import QRCode from 'qrcode';
|
|
3
|
-
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
4
|
-
import { translate } from '../i18n.js';
|
|
5
3
|
import { supportsPasskeys } from '../../accounts/account_store.js';
|
|
6
|
-
import {
|
|
4
|
+
import { accountPath } from '../account_paths.js';
|
|
5
|
+
import { translate } from '../i18n.js';
|
|
6
|
+
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
7
7
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
8
|
+
import { dispatchSecurityNotice } from '../security_notice_service.js';
|
|
8
9
|
import { requireSudo } from '../sudo_mode.js';
|
|
9
10
|
/** Desafio WebAuthn pendente (registro) guardado na sessão entre begin/finish. */
|
|
10
11
|
const PASSKEY_REG_CHALLENGE_KEY = 'authkit_passkey_reg_challenge';
|
|
12
|
+
/**
|
|
13
|
+
* `true` quando a requisição é uma NAVEGAÇÃO (form/browser) e não um XHR/fetch:
|
|
14
|
+
* o cliente aceita `text/html` e NÃO pede explicitamente `application/json`.
|
|
15
|
+
*
|
|
16
|
+
* Endpoints duais (JSON para o app React / redirect para o `<form>` clássico)
|
|
17
|
+
* decidem a resposta por aqui. Um `fetch` sem `Accept` explícito manda coringa
|
|
18
|
+
* (`*` + `/` + `*`) → não é navegação → JSON (mantém o comportamento de sempre
|
|
19
|
+
* da `mfa.edge`).
|
|
20
|
+
*/
|
|
21
|
+
function isNavigationRequest(ctx) {
|
|
22
|
+
const accept = (ctx.request.header('accept') ?? '').toLowerCase();
|
|
23
|
+
const wantsJson = accept.includes('application/json');
|
|
24
|
+
const wantsHtml = accept.includes('text/html');
|
|
25
|
+
return wantsHtml && !wantsJson;
|
|
26
|
+
}
|
|
11
27
|
/**
|
|
12
28
|
* Console de MFA da conta (atrás do account_auth middleware). Enrollment TOTP
|
|
13
29
|
* com QR, confirmação com código, exibição única dos recovery codes e disable.
|
|
@@ -90,8 +106,28 @@ export default class AccountMfaController {
|
|
|
90
106
|
if (accountForNotice) {
|
|
91
107
|
const ts = new Date().toISOString();
|
|
92
108
|
const ip = ctx.request.ip?.() ?? null;
|
|
93
|
-
await dispatchSecurityNotice(ctx, {
|
|
94
|
-
|
|
109
|
+
await dispatchSecurityNotice(ctx, {
|
|
110
|
+
account: { id: userId, email: accountForNotice.email },
|
|
111
|
+
kind: 'passkey_added',
|
|
112
|
+
ip,
|
|
113
|
+
timestamp: ts,
|
|
114
|
+
}, cfg.mail, cfg.audit);
|
|
115
|
+
await dispatchSecurityNotice(ctx, {
|
|
116
|
+
account: { id: userId, email: accountForNotice.email },
|
|
117
|
+
kind: 'mfa_enabled',
|
|
118
|
+
ip,
|
|
119
|
+
timestamp: ts,
|
|
120
|
+
}, cfg.mail, cfg.audit);
|
|
121
|
+
}
|
|
122
|
+
// Resposta DUAL. A cerimônia WebAuthn da tela built-in (`mfa.edge`) chama
|
|
123
|
+
// este endpoint por `fetch` e só olha `res.ok` — mas um `<form>` HTML
|
|
124
|
+
// clássico (progressive enhancement, ou host que POSTa a assertion sem JS)
|
|
125
|
+
// fica encarando o JSON `{ok:true}` cru. Navegação (o cliente ACEITA HTML e
|
|
126
|
+
// NÃO pede JSON) → redirect para a tela de MFA (respeitando `accountRoutes`);
|
|
127
|
+
// XHR/fetch → JSON de sempre. O fetch da view manda Accept coringa, então
|
|
128
|
+
// cai no ramo JSON e continua funcionando.
|
|
129
|
+
if (isNavigationRequest(ctx)) {
|
|
130
|
+
return ctx.response.redirect(accountPath('mfa'));
|
|
95
131
|
}
|
|
96
132
|
return { ok: true };
|
|
97
133
|
}
|
|
@@ -116,9 +152,14 @@ export default class AccountMfaController {
|
|
|
116
152
|
// Notificação de segurança: passkey_removed (best-effort).
|
|
117
153
|
const accountForNotice = await cfg.accountStore.findById(userId);
|
|
118
154
|
if (accountForNotice) {
|
|
119
|
-
await dispatchSecurityNotice(ctx, {
|
|
155
|
+
await dispatchSecurityNotice(ctx, {
|
|
156
|
+
account: { id: userId, email: accountForNotice.email },
|
|
157
|
+
kind: 'passkey_removed',
|
|
158
|
+
ip: ctx.request.ip?.() ?? null,
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
}, cfg.mail, cfg.audit);
|
|
120
161
|
}
|
|
121
|
-
return ctx.response.redirect('
|
|
162
|
+
return ctx.response.redirect(accountPath('mfa'));
|
|
122
163
|
}
|
|
123
164
|
/** POST /account/mfa/enroll — gera segredo pendente + QR e mostra a confirmação. */
|
|
124
165
|
async enroll(ctx) {
|
|
@@ -133,7 +174,7 @@ export default class AccountMfaController {
|
|
|
133
174
|
return sudoResultEnroll;
|
|
134
175
|
const started = await cfg.accountStore.startTotpEnrollment?.(userId);
|
|
135
176
|
if (!started) {
|
|
136
|
-
return ctx.response.redirect('
|
|
177
|
+
return ctx.response.redirect(accountPath('mfa'));
|
|
137
178
|
}
|
|
138
179
|
// QR renderizado server-side como data-URL e passado como prop.
|
|
139
180
|
const qrDataUrl = await QRCode.toDataURL(started.otpauthUri);
|
|
@@ -176,11 +217,16 @@ export default class AccountMfaController {
|
|
|
176
217
|
// Notificação de segurança: mfa_enabled (best-effort).
|
|
177
218
|
const accountForMfaNotice = await cfg.accountStore.findById(userId);
|
|
178
219
|
if (accountForMfaNotice) {
|
|
179
|
-
await dispatchSecurityNotice(ctx, {
|
|
220
|
+
await dispatchSecurityNotice(ctx, {
|
|
221
|
+
account: { id: userId, email: accountForMfaNotice.email },
|
|
222
|
+
kind: 'mfa_enabled',
|
|
223
|
+
ip: ctx.request.ip?.() ?? null,
|
|
224
|
+
timestamp: new Date().toISOString(),
|
|
225
|
+
}, cfg.mail, cfg.audit);
|
|
180
226
|
}
|
|
181
227
|
// Mostra os recovery codes UMA vez (flash) e volta pro estado "ativado".
|
|
182
228
|
ctx.session.flash('recoveryCodes', result.recoveryCodes ?? []);
|
|
183
|
-
return ctx.response.redirect('
|
|
229
|
+
return ctx.response.redirect(accountPath('mfa'));
|
|
184
230
|
}
|
|
185
231
|
/** POST /account/mfa/disable — desliga o MFA. */
|
|
186
232
|
async disable(ctx) {
|
|
@@ -201,8 +247,13 @@ export default class AccountMfaController {
|
|
|
201
247
|
// Notificação de segurança: mfa_disabled (best-effort).
|
|
202
248
|
const accountForDisableNotice = await cfg.accountStore.findById(userId);
|
|
203
249
|
if (accountForDisableNotice) {
|
|
204
|
-
await dispatchSecurityNotice(ctx, {
|
|
250
|
+
await dispatchSecurityNotice(ctx, {
|
|
251
|
+
account: { id: userId, email: accountForDisableNotice.email },
|
|
252
|
+
kind: 'mfa_disabled',
|
|
253
|
+
ip: ctx.request.ip?.() ?? null,
|
|
254
|
+
timestamp: new Date().toISOString(),
|
|
255
|
+
}, cfg.mail, cfg.audit);
|
|
205
256
|
}
|
|
206
|
-
return ctx.response.redirect('
|
|
257
|
+
return ctx.response.redirect(accountPath('mfa'));
|
|
207
258
|
}
|
|
208
259
|
}
|