@adonis-agora/authkit-server 0.47.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/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 +20 -11
- package/build/services/main.d.ts +1 -1
- package/build/services/main.js +2 -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 +6 -3
- package/build/src/host/account_login_url.js +16 -8
- 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.js +1 -3
- 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.js +1 -1
- 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 +47 -27
- package/build/src/host/controllers/account_security_controller.d.ts +2 -2
- package/build/src/host/controllers/account_security_controller.js +114 -115
- package/build/src/host/controllers/account_session_controller.js +5 -5
- package/build/src/host/controllers/account_tokens_controller.js +3 -2
- package/build/src/host/controllers/interaction_controller.js +59 -22
- 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/otp_lockout.js +1 -3
- package/build/src/host/rate_limit.js +1 -1
- package/build/src/host/register_auth_host.d.ts +30 -0
- package/build/src/host/register_auth_host.js +120 -74
- package/build/src/host/renderers/edge_renderer.js +15 -2
- package/build/src/host/renderers/inertia_renderer.d.ts +9 -1
- package/build/src/host/renderers/inertia_renderer.js +9 -1
- 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,22 +1,23 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { requireSudo } from
|
|
19
|
-
import {
|
|
1
|
+
import '../augmentations.js';
|
|
2
|
+
import { supportsAccountDeletion, supportsAccountSecurity, supportsPasswordHistory, supportsProfile, } from '../../accounts/account_store.js';
|
|
3
|
+
import { PasswordPolicyError } from '../../password/password_manager.js';
|
|
4
|
+
import { AccountDeletionService } from '../account_deletion_service.js';
|
|
5
|
+
import { AccountExportService } from '../account_export_service.js';
|
|
6
|
+
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
7
|
+
import { accountPath } from '../account_paths.js';
|
|
8
|
+
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
9
|
+
import { syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
10
|
+
import { AvatarUploadError, isAvatarUploadSupported, storeAvatar } from '../avatar_storage.js';
|
|
11
|
+
import { sendEmailChangeConfirmationEmail, sendEmailChangeNoticeEmail, sendEmailChangedCompletedEmail, } from '../default_mailer.js';
|
|
12
|
+
import { translate } from '../i18n.js';
|
|
13
|
+
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
14
|
+
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
15
|
+
import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '../runtime_toggles.js';
|
|
16
|
+
import { dispatchSecurityNotice } from '../security_notice_service.js';
|
|
17
|
+
import { enrichSessionsWithContext } from '../session_context.js';
|
|
18
|
+
import { requireSudo } from '../sudo_mode.js';
|
|
19
|
+
import { TRUSTED_DEVICE_COOKIE } from '../trusted_device.js';
|
|
20
|
+
import { changeEmailValidator, changePasswordValidator, deleteAccountValidator, updateProfileValidator, } from '../validators.js';
|
|
20
21
|
/** Resolve os password history settings em runtime (fail-safe). */
|
|
21
22
|
async function resolvePasswordHistorySettings(ctx) {
|
|
22
23
|
try {
|
|
@@ -52,7 +53,7 @@ async function resolveEmailChangeSettings(ctx) {
|
|
|
52
53
|
*/
|
|
53
54
|
export default class AccountSecurityController {
|
|
54
55
|
async index(ctx) {
|
|
55
|
-
const service = await ctx.containerResolver.make(
|
|
56
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
56
57
|
const cfg = service.config;
|
|
57
58
|
const render = cfg.render;
|
|
58
59
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
@@ -64,35 +65,35 @@ export default class AccountSecurityController {
|
|
|
64
65
|
const ownSessions = sessionsSupported
|
|
65
66
|
? await enrichSessionsWithContext(cfg, userId, await adminSessions.listSessions(userId))
|
|
66
67
|
: [];
|
|
67
|
-
return render(ctx,
|
|
68
|
+
return render(ctx, 'account/security', {
|
|
68
69
|
csrfToken: ctx.request.csrfToken,
|
|
69
70
|
supported: supportsAccountSecurity(cfg.accountStore),
|
|
70
71
|
profileSupported: supportsProfile(cfg.accountStore),
|
|
71
72
|
// Só mostramos o input de arquivo se ALGUM backend (drive OU media) puder armazenar.
|
|
72
73
|
avatarUploadSupported: await isAvatarUploadSupported(cfg.uploads),
|
|
73
|
-
email: account?.email ??
|
|
74
|
-
name: account?.name ??
|
|
75
|
-
avatarUrl: account?.avatarUrl ??
|
|
76
|
-
passwordChanged: ctx.session.flashMessages.get(
|
|
77
|
-
emailChangeRequested: ctx.session.flashMessages.get(
|
|
78
|
-
emailChanged: ctx.session.flashMessages.get(
|
|
79
|
-
profileUpdated: ctx.session.flashMessages.get(
|
|
80
|
-
error: ctx.session.flashMessages.get(
|
|
74
|
+
email: account?.email ?? '',
|
|
75
|
+
name: account?.name ?? '',
|
|
76
|
+
avatarUrl: account?.avatarUrl ?? '',
|
|
77
|
+
passwordChanged: ctx.session.flashMessages.get('passwordChanged') ?? null,
|
|
78
|
+
emailChangeRequested: ctx.session.flashMessages.get('emailChangeRequested') ?? null,
|
|
79
|
+
emailChanged: ctx.session.flashMessages.get('emailChanged') ?? null,
|
|
80
|
+
profileUpdated: ctx.session.flashMessages.get('profileUpdated') ?? null,
|
|
81
|
+
error: ctx.session.flashMessages.get('securityError') ?? null,
|
|
81
82
|
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
82
|
-
trustedDevicesRevoked: ctx.session.flashMessages.get(
|
|
83
|
+
trustedDevicesRevoked: ctx.session.flashMessages.get('trustedDevicesRevoked') ?? null,
|
|
83
84
|
sessionsSupported,
|
|
84
85
|
sessions: ownSessions.map((s) => ({
|
|
85
|
-
loginTs: s.loginTs ? new Date(s.loginTs * 1000).toISOString() :
|
|
86
|
-
browser: s.browser ??
|
|
87
|
-
os: s.os ??
|
|
88
|
-
ip: s.ip ??
|
|
89
|
-
location: s.location ??
|
|
86
|
+
loginTs: s.loginTs ? new Date(s.loginTs * 1000).toISOString() : '',
|
|
87
|
+
browser: s.browser ?? '',
|
|
88
|
+
os: s.os ?? '',
|
|
89
|
+
ip: s.ip ?? '',
|
|
90
|
+
location: s.location ?? '',
|
|
90
91
|
})),
|
|
91
92
|
// Export de dados (portabilidade) sempre disponível para a conta logada.
|
|
92
93
|
exportSupported: true,
|
|
93
94
|
// Deleção de conta (LGPD): só quando o store suporta hard delete.
|
|
94
95
|
deletionSupported: supportsAccountDeletion(cfg.accountStore),
|
|
95
|
-
deleteError: ctx.session.flashMessages.get(
|
|
96
|
+
deleteError: ctx.session.flashMessages.get('deleteError') ?? null,
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
/**
|
|
@@ -101,20 +102,20 @@ export default class AccountSecurityController {
|
|
|
101
102
|
* NUNCA inclui segredos. Audita `account.exported`.
|
|
102
103
|
*/
|
|
103
104
|
async exportData(ctx) {
|
|
104
|
-
const service = await ctx.containerResolver.make(
|
|
105
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
105
106
|
const cfg = service.config;
|
|
106
107
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
107
108
|
if (cfg.accountLifecycle?.durable) {
|
|
108
109
|
// OPT-IN durável: enfileira o workflow de export (coleta + persiste artefato
|
|
109
110
|
// + entrega async). Isolado no subpath durável — só carregado neste ramo.
|
|
110
|
-
const { resolveWorkflowEngine, enqueueAccountExport } = await import(
|
|
111
|
+
const { resolveWorkflowEngine, enqueueAccountExport } = await import('../durable/index.js');
|
|
111
112
|
const engine = await resolveWorkflowEngine(ctx.containerResolver);
|
|
112
113
|
await enqueueAccountExport(engine, {
|
|
113
114
|
accountId: userId,
|
|
114
115
|
ip: ctx.request.ip?.() ?? null,
|
|
115
116
|
});
|
|
116
|
-
ctx.session.flash(
|
|
117
|
-
return ctx.response.redirect(
|
|
117
|
+
ctx.session.flash('exportRequested', translate(cfg.messages, 'account.export.requested'));
|
|
118
|
+
return ctx.response.redirect(accountPath('security'));
|
|
118
119
|
}
|
|
119
120
|
// Caminho SÍNCRONO de sempre (byte-idêntico): download inline do JSON.
|
|
120
121
|
const payload = await new AccountExportService(service).export(userId);
|
|
@@ -122,12 +123,12 @@ export default class AccountSecurityController {
|
|
|
122
123
|
return ctx.response.notFound();
|
|
123
124
|
}
|
|
124
125
|
await cfg.audit?.record({
|
|
125
|
-
type:
|
|
126
|
+
type: 'account.exported',
|
|
126
127
|
accountId: userId,
|
|
127
128
|
ip: ctx.request.ip?.() ?? null,
|
|
128
129
|
});
|
|
129
|
-
ctx.response.header(
|
|
130
|
-
ctx.response.header(
|
|
130
|
+
ctx.response.header('content-type', 'application/json; charset=utf-8');
|
|
131
|
+
ctx.response.header('content-disposition', `attachment; filename="authkit-data-export-${userId}.json"`);
|
|
131
132
|
return ctx.response.send(JSON.stringify(payload, null, 2));
|
|
132
133
|
}
|
|
133
134
|
/**
|
|
@@ -137,12 +138,12 @@ export default class AccountSecurityController {
|
|
|
137
138
|
* completo e encerra a sessão.
|
|
138
139
|
*/
|
|
139
140
|
async deleteAccount(ctx) {
|
|
140
|
-
const service = await ctx.containerResolver.make(
|
|
141
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
141
142
|
const cfg = service.config;
|
|
142
143
|
const store = cfg.accountStore;
|
|
143
144
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
144
145
|
if (!supportsAccountDeletion(store)) {
|
|
145
|
-
return ctx.response.redirect(
|
|
146
|
+
return ctx.response.redirect(accountPath('security'));
|
|
146
147
|
}
|
|
147
148
|
// Sudo mode gate (defesa em profundidade — confirmação inline continua abaixo).
|
|
148
149
|
const sudoSettingsDel = await resolveRuntimeSettings(ctx);
|
|
@@ -161,25 +162,24 @@ export default class AccountSecurityController {
|
|
|
161
162
|
confirmed = !!(await store.verifyCredentials(account.email, currentPassword));
|
|
162
163
|
}
|
|
163
164
|
if (!confirmed && confirmEmail) {
|
|
164
|
-
confirmed =
|
|
165
|
-
confirmEmail.trim().toLowerCase() === account.email.toLowerCase();
|
|
165
|
+
confirmed = confirmEmail.trim().toLowerCase() === account.email.toLowerCase();
|
|
166
166
|
}
|
|
167
167
|
if (!confirmed) {
|
|
168
|
-
ctx.session.flash(
|
|
169
|
-
return ctx.response.redirect(
|
|
168
|
+
ctx.session.flash('deleteError', translate(cfg.messages, 'account.delete.invalid_confirmation'));
|
|
169
|
+
return ctx.response.redirect(accountPath('security'));
|
|
170
170
|
}
|
|
171
171
|
const actor = {
|
|
172
172
|
actorId: userId,
|
|
173
173
|
ip: ctx.request.ip?.() ?? null,
|
|
174
|
-
source:
|
|
174
|
+
source: 'self',
|
|
175
175
|
};
|
|
176
176
|
if (cfg.accountLifecycle?.durable) {
|
|
177
177
|
// OPT-IN durável (self-service): logout IMEDIATO (revoga as sessões/grants
|
|
178
178
|
// OIDC do ator de forma síncrona) + enfileira o resto do cascade async.
|
|
179
179
|
// Isolado no subpath durável — só carregado neste ramo.
|
|
180
|
-
const { resolveWorkflowEngine, enqueueAccountDeletion } = await import(
|
|
180
|
+
const { resolveWorkflowEngine, enqueueAccountDeletion } = await import('../durable/index.js');
|
|
181
181
|
try {
|
|
182
|
-
const { revokeSessions } = await import(
|
|
182
|
+
const { revokeSessions } = await import('../account_deletion_ops.js');
|
|
183
183
|
await revokeSessions(service, userId);
|
|
184
184
|
}
|
|
185
185
|
catch {
|
|
@@ -196,7 +196,7 @@ export default class AccountSecurityController {
|
|
|
196
196
|
// Encerra a sessão e leva ao login com a mensagem de sucesso.
|
|
197
197
|
ctx.session.forget(ACCOUNT_SESSION_KEY);
|
|
198
198
|
await syncAdonisAuthLogout(ctx, cfg);
|
|
199
|
-
ctx.session.flash(
|
|
199
|
+
ctx.session.flash('accountDeleted', translate(cfg.messages, 'account.delete.deleted'));
|
|
200
200
|
return ctx.response.redirect(getAccountLoginUrl());
|
|
201
201
|
}
|
|
202
202
|
/**
|
|
@@ -206,52 +206,52 @@ export default class AccountSecurityController {
|
|
|
206
206
|
* server-side; re-enrolar o MFA invalida a confiança em TODOS os dispositivos.
|
|
207
207
|
*/
|
|
208
208
|
async revokeTrustedDevices(ctx) {
|
|
209
|
-
const service = await ctx.containerResolver.make(
|
|
209
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
210
210
|
const cfg = service.config;
|
|
211
211
|
ctx.response.clearCookie(TRUSTED_DEVICE_COOKIE);
|
|
212
212
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
213
213
|
await cfg.audit?.record({
|
|
214
|
-
type:
|
|
214
|
+
type: 'trusted_device.revoked',
|
|
215
215
|
accountId: userId,
|
|
216
216
|
ip: ctx.request.ip?.() ?? null,
|
|
217
217
|
});
|
|
218
|
-
ctx.session.flash(
|
|
219
|
-
return ctx.response.redirect(
|
|
218
|
+
ctx.session.flash('trustedDevicesRevoked', translate(cfg.messages, 'account.security.trusted_devices_revoked'));
|
|
219
|
+
return ctx.response.redirect(accountPath('security'));
|
|
220
220
|
}
|
|
221
221
|
/** POST /account/security/profile — atualiza nome + avatar do próprio perfil. */
|
|
222
222
|
async updateProfile(ctx) {
|
|
223
|
-
const service = await ctx.containerResolver.make(
|
|
223
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
224
224
|
const cfg = service.config;
|
|
225
225
|
const store = cfg.accountStore;
|
|
226
226
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
227
227
|
if (!supportsProfile(store)) {
|
|
228
|
-
return ctx.response.redirect(
|
|
228
|
+
return ctx.response.redirect(accountPath('security'));
|
|
229
229
|
}
|
|
230
230
|
const { name, avatarUrl } = await ctx.request.validateUsing(updateProfileValidator);
|
|
231
231
|
// Upload de avatar via o drive do app (opt-in pela presença do arquivo).
|
|
232
232
|
// Se um arquivo for enviado e o drive estiver disponível, a URL resultante
|
|
233
233
|
// tem prioridade sobre o input de URL; senão caímos no avatarUrl (texto).
|
|
234
234
|
let resolvedAvatarUrl = avatarUrl ?? null;
|
|
235
|
-
let via =
|
|
236
|
-
const file = ctx.request.file(
|
|
235
|
+
let via = 'url';
|
|
236
|
+
const file = ctx.request.file('avatar', {
|
|
237
237
|
size: `${cfg.uploads.avatars.maxSizeMb}mb`,
|
|
238
|
-
extnames: [
|
|
238
|
+
extnames: ['jpg', 'jpeg', 'png', 'webp'],
|
|
239
239
|
});
|
|
240
240
|
if (file) {
|
|
241
241
|
try {
|
|
242
242
|
const uploadedUrl = await storeAvatar(ctx, cfg.uploads, file, userId, {
|
|
243
|
-
extname: translate(cfg.messages,
|
|
244
|
-
size: translate(cfg.messages,
|
|
243
|
+
extname: translate(cfg.messages, 'account.profile.avatar_invalid_type'),
|
|
244
|
+
size: translate(cfg.messages, 'account.profile.avatar_too_large'),
|
|
245
245
|
});
|
|
246
246
|
if (uploadedUrl) {
|
|
247
247
|
resolvedAvatarUrl = uploadedUrl;
|
|
248
|
-
via =
|
|
248
|
+
via = 'upload';
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
catch (error) {
|
|
252
252
|
if (error instanceof AvatarUploadError) {
|
|
253
|
-
ctx.session.flash(
|
|
254
|
-
return ctx.response.redirect(
|
|
253
|
+
ctx.session.flash('securityError', error.message);
|
|
254
|
+
return ctx.response.redirect(accountPath('security'));
|
|
255
255
|
}
|
|
256
256
|
throw error;
|
|
257
257
|
}
|
|
@@ -263,21 +263,21 @@ export default class AccountSecurityController {
|
|
|
263
263
|
avatarUrl: resolvedAvatarUrl,
|
|
264
264
|
});
|
|
265
265
|
await cfg.audit?.record({
|
|
266
|
-
type:
|
|
266
|
+
type: 'profile.updated',
|
|
267
267
|
accountId: userId,
|
|
268
268
|
ip: ctx.request.ip?.() ?? null,
|
|
269
269
|
metadata: { via },
|
|
270
270
|
});
|
|
271
|
-
ctx.session.flash(
|
|
272
|
-
return ctx.response.redirect(
|
|
271
|
+
ctx.session.flash('profileUpdated', translate(cfg.messages, 'account.profile.updated'));
|
|
272
|
+
return ctx.response.redirect(accountPath('security'));
|
|
273
273
|
}
|
|
274
274
|
async changePassword(ctx) {
|
|
275
|
-
const service = await ctx.containerResolver.make(
|
|
275
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
276
276
|
const cfg = service.config;
|
|
277
277
|
const store = cfg.accountStore;
|
|
278
278
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
279
279
|
if (!supportsAccountSecurity(store)) {
|
|
280
|
-
return ctx.response.redirect(
|
|
280
|
+
return ctx.response.redirect(accountPath('security'));
|
|
281
281
|
}
|
|
282
282
|
// Sudo mode gate (defesa em profundidade — a verificação de senha ATUAL continua abaixo).
|
|
283
283
|
const sudoSettings = await resolveRuntimeSettings(ctx);
|
|
@@ -287,12 +287,10 @@ export default class AccountSecurityController {
|
|
|
287
287
|
const { currentPassword, newPassword } = await ctx.request.validateUsing(changePasswordValidator);
|
|
288
288
|
const account = await store.findById(userId);
|
|
289
289
|
// Confirma a senha ATUAL pelo e-mail da conta.
|
|
290
|
-
const verified = account
|
|
291
|
-
? await store.verifyCredentials(account.email, currentPassword)
|
|
292
|
-
: null;
|
|
290
|
+
const verified = account ? await store.verifyCredentials(account.email, currentPassword) : null;
|
|
293
291
|
if (!verified) {
|
|
294
|
-
ctx.session.flash(
|
|
295
|
-
return ctx.response.redirect(
|
|
292
|
+
ctx.session.flash('securityError', translate(cfg.messages, 'errors.invalid_credentials'));
|
|
293
|
+
return ctx.response.redirect(accountPath('security'));
|
|
296
294
|
}
|
|
297
295
|
// Verificação de histórico de senhas (disallow_password_reuse).
|
|
298
296
|
// Capability-probed: sem `isPasswordReused` ou com setting desligada → no-op.
|
|
@@ -301,13 +299,14 @@ export default class AccountSecurityController {
|
|
|
301
299
|
if (histSettings.enabled) {
|
|
302
300
|
// Aplica o pepper à senha candidata antes de comparar com os hashes históricos
|
|
303
301
|
// (que foram gravados já com pepper). Pepper ausente → identidade.
|
|
304
|
-
const pepperedNew = cfg.accountStore.__passwordManager?.applyCurrentPepper?.(newPassword) ??
|
|
302
|
+
const pepperedNew = cfg.accountStore.__passwordManager?.applyCurrentPepper?.(newPassword) ??
|
|
303
|
+
newPassword;
|
|
305
304
|
const reused = await store.isPasswordReused(userId, pepperedNew, histSettings.count);
|
|
306
305
|
if (reused) {
|
|
307
|
-
ctx.session.flash(
|
|
306
|
+
ctx.session.flash('securityError', translate(cfg.messages, 'password.reused', {
|
|
308
307
|
count: histSettings.count,
|
|
309
308
|
}));
|
|
310
|
-
return ctx.response.redirect(
|
|
309
|
+
return ctx.response.redirect(accountPath('security'));
|
|
311
310
|
}
|
|
312
311
|
// Grava o hash ATUAL no histórico antes de trocar.
|
|
313
312
|
// O hash atual está na linha do DB — buscamos via store.findById + raw model.
|
|
@@ -324,13 +323,13 @@ export default class AccountSecurityController {
|
|
|
324
323
|
catch (error) {
|
|
325
324
|
// Política de senha violada → flash com a regra e volta à tela de segurança.
|
|
326
325
|
if (error instanceof PasswordPolicyError) {
|
|
327
|
-
ctx.session.flash(
|
|
328
|
-
return ctx.response.redirect(
|
|
326
|
+
ctx.session.flash('securityError', translate(cfg.messages, error.key, error.params));
|
|
327
|
+
return ctx.response.redirect(accountPath('security'));
|
|
329
328
|
}
|
|
330
329
|
throw error;
|
|
331
330
|
}
|
|
332
331
|
await cfg.audit?.record({
|
|
333
|
-
type:
|
|
332
|
+
type: 'password.changed',
|
|
334
333
|
accountId: userId,
|
|
335
334
|
ip: ctx.request.ip?.() ?? null,
|
|
336
335
|
});
|
|
@@ -343,7 +342,7 @@ export default class AccountSecurityController {
|
|
|
343
342
|
const sessions = new AdminSessionsService(service);
|
|
344
343
|
const result = await sessions.revokeAll(userId);
|
|
345
344
|
await cfg.audit?.record({
|
|
346
|
-
type:
|
|
345
|
+
type: 'session.revoked_all',
|
|
347
346
|
accountId: userId,
|
|
348
347
|
actorId: userId,
|
|
349
348
|
ip: ctx.request.ip?.() ?? null,
|
|
@@ -352,7 +351,7 @@ export default class AccountSecurityController {
|
|
|
352
351
|
grants: result.grants,
|
|
353
352
|
accessTokens: result.accessTokens,
|
|
354
353
|
refreshTokens: result.refreshTokens,
|
|
355
|
-
source:
|
|
354
|
+
source: 'password-change',
|
|
356
355
|
},
|
|
357
356
|
});
|
|
358
357
|
}
|
|
@@ -363,21 +362,21 @@ export default class AccountSecurityController {
|
|
|
363
362
|
if (account) {
|
|
364
363
|
await dispatchSecurityNotice(ctx, {
|
|
365
364
|
account: { id: userId, email: account.email },
|
|
366
|
-
kind:
|
|
365
|
+
kind: 'password_changed',
|
|
367
366
|
ip: ctx.request.ip?.() ?? null,
|
|
368
367
|
timestamp: new Date().toISOString(),
|
|
369
368
|
}, cfg.mail, cfg.audit);
|
|
370
369
|
}
|
|
371
|
-
ctx.session.flash(
|
|
372
|
-
return ctx.response.redirect(
|
|
370
|
+
ctx.session.flash('passwordChanged', translate(cfg.messages, 'account.security.password_changed'));
|
|
371
|
+
return ctx.response.redirect(accountPath('security'));
|
|
373
372
|
}
|
|
374
373
|
async changeEmail(ctx) {
|
|
375
|
-
const service = await ctx.containerResolver.make(
|
|
374
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
376
375
|
const cfg = service.config;
|
|
377
376
|
const store = cfg.accountStore;
|
|
378
377
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
379
378
|
if (!supportsAccountSecurity(store)) {
|
|
380
|
-
return ctx.response.redirect(
|
|
379
|
+
return ctx.response.redirect(accountPath('security'));
|
|
381
380
|
}
|
|
382
381
|
// Sudo mode gate.
|
|
383
382
|
const sudoSettingsEmail = await resolveRuntimeSettings(ctx);
|
|
@@ -387,45 +386,45 @@ export default class AccountSecurityController {
|
|
|
387
386
|
// Resolve os settings de troca de e-mail em runtime.
|
|
388
387
|
const emailChangeSettings = await resolveEmailChangeSettings(ctx);
|
|
389
388
|
if (!emailChangeSettings.enabled) {
|
|
390
|
-
ctx.session.flash(
|
|
391
|
-
return ctx.response.redirect(
|
|
389
|
+
ctx.session.flash('securityError', translate(cfg.messages, 'account.security.email_change_disabled'));
|
|
390
|
+
return ctx.response.redirect(accountPath('security'));
|
|
392
391
|
}
|
|
393
392
|
const { currentPassword, newEmail } = await ctx.request.validateUsing(changeEmailValidator);
|
|
394
393
|
const account = await store.findById(userId);
|
|
395
394
|
// requirePassword: se ligado (default true), exige senha atual.
|
|
396
395
|
if (emailChangeSettings.requirePassword) {
|
|
397
396
|
if (!currentPassword) {
|
|
398
|
-
ctx.session.flash(
|
|
399
|
-
return ctx.response.redirect(
|
|
397
|
+
ctx.session.flash('securityError', translate(cfg.messages, 'errors.invalid_credentials'));
|
|
398
|
+
return ctx.response.redirect(accountPath('security'));
|
|
400
399
|
}
|
|
401
400
|
const verified = account
|
|
402
401
|
? await store.verifyCredentials(account.email, currentPassword)
|
|
403
402
|
: null;
|
|
404
403
|
if (!verified) {
|
|
405
|
-
ctx.session.flash(
|
|
406
|
-
return ctx.response.redirect(
|
|
404
|
+
ctx.session.flash('securityError', translate(cfg.messages, 'errors.invalid_credentials'));
|
|
405
|
+
return ctx.response.redirect(accountPath('security'));
|
|
407
406
|
}
|
|
408
407
|
}
|
|
409
408
|
const issued = await store.requestEmailChange(userId, newEmail);
|
|
410
409
|
if (!issued) {
|
|
411
|
-
ctx.session.flash(
|
|
412
|
-
return ctx.response.redirect(
|
|
410
|
+
ctx.session.flash('securityError', translate(cfg.messages, 'errors.email_taken'));
|
|
411
|
+
return ctx.response.redirect(accountPath('security'));
|
|
413
412
|
}
|
|
414
413
|
await cfg.audit?.record({
|
|
415
|
-
type:
|
|
414
|
+
type: 'email_change.requested',
|
|
416
415
|
accountId: userId,
|
|
417
416
|
email: newEmail,
|
|
418
417
|
ip: ctx.request.ip?.() ?? null,
|
|
419
418
|
});
|
|
420
419
|
const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
|
|
421
|
-
const confirmUrl = `${origin}
|
|
420
|
+
const confirmUrl = `${origin}${accountPath('emailConfirm')}?token=${encodeURIComponent(issued.token)}`;
|
|
422
421
|
// 1. Link de confirmação → NOVO e-mail (hook onEmailChangeConfirm > onEmailVerification > default).
|
|
423
422
|
if (cfg.mail?.onEmailChangeConfirm) {
|
|
424
423
|
await cfg.mail.onEmailChangeConfirm({
|
|
425
424
|
email: newEmail,
|
|
426
425
|
confirmUrl,
|
|
427
426
|
token: issued.token,
|
|
428
|
-
oldEmail: account?.email ??
|
|
427
|
+
oldEmail: account?.email ?? '',
|
|
429
428
|
});
|
|
430
429
|
}
|
|
431
430
|
else if (cfg.mail?.onEmailVerification) {
|
|
@@ -454,22 +453,22 @@ export default class AccountSecurityController {
|
|
|
454
453
|
});
|
|
455
454
|
}
|
|
456
455
|
}
|
|
457
|
-
ctx.session.flash(
|
|
456
|
+
ctx.session.flash('emailChangeRequested', translate(cfg.messages, 'account.security.email_change_requested', {
|
|
458
457
|
email: newEmail,
|
|
459
458
|
}));
|
|
460
|
-
return ctx.response.redirect(
|
|
459
|
+
return ctx.response.redirect(accountPath('security'));
|
|
461
460
|
}
|
|
462
461
|
/**
|
|
463
462
|
* POST /account/security/email/cancel — cancela uma troca de e-mail pendente
|
|
464
463
|
* (limpa o token pending sem alterar o e-mail da conta).
|
|
465
464
|
*/
|
|
466
465
|
async cancelEmailChange(ctx) {
|
|
467
|
-
const service = await ctx.containerResolver.make(
|
|
466
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
468
467
|
const cfg = service.config;
|
|
469
468
|
const store = cfg.accountStore;
|
|
470
469
|
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
471
470
|
if (!supportsAccountSecurity(store)) {
|
|
472
|
-
return ctx.response.redirect(
|
|
471
|
+
return ctx.response.redirect(accountPath('security'));
|
|
473
472
|
}
|
|
474
473
|
// Implementação: issuePendingToken(userId, null) limpa o token; reutilizamos
|
|
475
474
|
// requestEmailChange com um valor sentinela → não, melhor usar cancelEmailChange
|
|
@@ -478,7 +477,7 @@ export default class AccountSecurityController {
|
|
|
478
477
|
// A forma mais limpa é verificar se o store tem cancelEmailChange; se não tiver,
|
|
479
478
|
// chamamos requestEmailChange com o próprio e-mail atual (limpa o pending antigo).
|
|
480
479
|
const cancelFn = store.cancelEmailChange;
|
|
481
|
-
if (typeof cancelFn ===
|
|
480
|
+
if (typeof cancelFn === 'function') {
|
|
482
481
|
await cancelFn.call(store, userId);
|
|
483
482
|
}
|
|
484
483
|
else {
|
|
@@ -492,27 +491,27 @@ export default class AccountSecurityController {
|
|
|
492
491
|
}
|
|
493
492
|
}
|
|
494
493
|
await cfg.audit?.record({
|
|
495
|
-
type:
|
|
494
|
+
type: 'email_change.cancelled',
|
|
496
495
|
accountId: userId,
|
|
497
496
|
ip: ctx.request.ip?.() ?? null,
|
|
498
497
|
});
|
|
499
|
-
ctx.session.flash(
|
|
500
|
-
return ctx.response.redirect(
|
|
498
|
+
ctx.session.flash('emailChangeRequested', translate(cfg.messages, 'account.security.email_change_cancelled'));
|
|
499
|
+
return ctx.response.redirect(accountPath('security'));
|
|
501
500
|
}
|
|
502
501
|
/** GET /account/email/confirm?token=... — consome o token e aplica o novo e-mail. */
|
|
503
502
|
async confirmEmail(ctx) {
|
|
504
|
-
const service = await ctx.containerResolver.make(
|
|
503
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
505
504
|
const cfg = service.config;
|
|
506
505
|
const store = cfg.accountStore;
|
|
507
506
|
const render = cfg.render;
|
|
508
507
|
if (!supportsAccountSecurity(store)) {
|
|
509
|
-
return render(ctx,
|
|
508
|
+
return render(ctx, 'account/email-confirmed', { ok: false });
|
|
510
509
|
}
|
|
511
|
-
const token = ctx.request.qs().token ??
|
|
510
|
+
const token = ctx.request.qs().token ?? '';
|
|
512
511
|
const result = await store.confirmEmailChange(token);
|
|
513
512
|
if (result.ok) {
|
|
514
513
|
await cfg.audit?.record({
|
|
515
|
-
type:
|
|
514
|
+
type: 'email_change.confirmed',
|
|
516
515
|
accountId: result.account.id,
|
|
517
516
|
email: result.newEmail,
|
|
518
517
|
ip: ctx.request.ip?.() ?? null,
|
|
@@ -538,12 +537,12 @@ export default class AccountSecurityController {
|
|
|
538
537
|
// Notificação de segurança ao NOVO e-mail (email_changed) — best-effort.
|
|
539
538
|
await dispatchSecurityNotice(ctx, {
|
|
540
539
|
account: { id: result.account.id, email: result.newEmail },
|
|
541
|
-
kind:
|
|
540
|
+
kind: 'email_changed',
|
|
542
541
|
ip: ctx.request.ip?.() ?? null,
|
|
543
542
|
timestamp: new Date().toISOString(),
|
|
544
543
|
metadata: { oldEmail: result.oldEmail, newEmail: result.newEmail },
|
|
545
544
|
}, cfg.mail, cfg.audit);
|
|
546
545
|
}
|
|
547
|
-
return render(ctx,
|
|
546
|
+
return render(ctx, 'account/email-confirmed', { ok: result.ok });
|
|
548
547
|
}
|
|
549
548
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
|
-
import {
|
|
2
|
+
import { accountHome } from '../account_home.js';
|
|
3
|
+
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
4
|
+
import { syncAdonisAuthLogin, syncAdonisAuthLogout } from '../adonis_auth_sync.js';
|
|
3
5
|
import { translate } from '../i18n.js';
|
|
4
6
|
import { attemptPasswordLogin } from '../login_attempt.js';
|
|
5
7
|
import { notifyLoginSuccess } from '../login_notify.js';
|
|
6
|
-
import {
|
|
7
|
-
import { accountHome } from '../account_home.js';
|
|
8
|
-
import { getAccountLoginUrl } from '../account_login_url.js';
|
|
8
|
+
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
9
9
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
10
|
-
import {
|
|
10
|
+
import { markSudo } from '../sudo_mode.js';
|
|
11
11
|
/**
|
|
12
12
|
* Valida um valor de `return_to` recebido da query-string ou de um campo hidden.
|
|
13
13
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
|
+
import { accountPath } from '../account_paths.js';
|
|
2
3
|
import { ACCOUNT_SESSION_KEY } from '../middleware/account_auth.js';
|
|
3
4
|
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
4
5
|
import { requireSudo } from '../sudo_mode.js';
|
|
@@ -49,7 +50,7 @@ export default class AccountTokensController {
|
|
|
49
50
|
ip: ctx.request.ip?.() ?? null,
|
|
50
51
|
metadata: { patId: pat.id, name: pat.name },
|
|
51
52
|
});
|
|
52
|
-
return ctx.response.redirect('
|
|
53
|
+
return ctx.response.redirect(accountPath('tokens'));
|
|
53
54
|
}
|
|
54
55
|
async destroy(ctx) {
|
|
55
56
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
@@ -73,6 +74,6 @@ export default class AccountTokensController {
|
|
|
73
74
|
metadata: { patId },
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
|
-
return ctx.response.redirect('
|
|
77
|
+
return ctx.response.redirect(accountPath('tokens'));
|
|
77
78
|
}
|
|
78
79
|
}
|