@adonis-agora/authkit-server 0.59.0 → 0.60.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/index.d.ts +4 -2
- package/build/index.js +3 -1
- package/build/src/accounts/account_store.d.ts +20 -1
- package/build/src/accounts/account_store.js +4 -0
- package/build/src/accounts/lucid_account_store.js +4 -0
- package/build/src/accounts/lucid_store/login_methods.d.ts +11 -0
- package/build/src/accounts/lucid_store/login_methods.js +32 -0
- package/build/src/host/account_api/account_api_controller.d.ts +38 -0
- package/build/src/host/account_api/account_api_controller.js +84 -3
- package/build/src/host/controllers/interaction_controller.js +159 -12
- package/build/src/host/controllers/social_controller.js +29 -1
- package/build/src/host/register_auth_host.js +3 -0
- package/build/src/host/ui-dist/assets/{index-6cE5JMyP.js → index-Cb38N16r.js} +1 -1
- package/build/src/host/ui-dist/index.html +1 -1
- package/build/src/host/user_login_methods.d.ts +94 -0
- package/build/src/host/user_login_methods.js +132 -0
- package/package.json +2 -2
package/build/index.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export { lucidAccountStore, appKeyEncrypter } from './src/accounts/lucid_account
|
|
|
23
23
|
export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
24
24
|
export type { LucidStoresModels, LucidStoresOptions, LucidStoresResult, } from './src/accounts/lucid_stores.js';
|
|
25
25
|
export type { LucidAccountStoreOptions, AccountSecretEncrypter, } from './src/accounts/lucid_account_store.js';
|
|
26
|
-
export type { AccountStore, CoreAccountStore, AdminCapability, MfaCapability, WebauthnCapability, ProviderIdentityCapability, ProviderIdentitySummary, AccountSecurityCapability, AccountStatusCapability, ProfileCapability, MagicLinkCapability, OtpLoginCapability, OtpLoginVerifyResult, EmailVerificationStatusCapability, AccountDeletionCapability, AccountImportCapability, ImportAccountInput, AuthAccount, CreateAccountInput, LinkProviderIdentityInput, ListAccountsParams, Paginated, PasskeySummary, } from './src/accounts/account_store.js';
|
|
27
|
-
export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, supportsAccountStatus, supportsProfile, supportsMagicLink, supportsOtpLogin, supportsEmailVerificationStatus, supportsAccountDeletion, supportsAccountImport, } from './src/accounts/account_store.js';
|
|
26
|
+
export type { AccountStore, CoreAccountStore, AdminCapability, MfaCapability, WebauthnCapability, ProviderIdentityCapability, ProviderIdentitySummary, AccountSecurityCapability, AccountStatusCapability, ProfileCapability, MagicLinkCapability, OtpLoginCapability, OtpLoginVerifyResult, EmailVerificationStatusCapability, AccountDeletionCapability, AccountImportCapability, ImportAccountInput, AuthAccount, CreateAccountInput, LinkProviderIdentityInput, ListAccountsParams, Paginated, PasskeySummary, LoginMethodsPreferenceCapability, } from './src/accounts/account_store.js';
|
|
27
|
+
export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, supportsAccountStatus, supportsProfile, supportsMagicLink, supportsOtpLogin, supportsEmailVerificationStatus, supportsAccountDeletion, supportsAccountImport, supportsLoginMethodsPreference, } from './src/accounts/account_store.js';
|
|
28
28
|
export { type OtpLoginConfigInput, type ResolvedOtpLoginConfig, type OtpVerifyOutcome, resolveOtpLoginConfig, generateOtpCode, evaluateLoginOtp, OTP_LOGIN_DEFAULTS, } from './src/host/otp_login.js';
|
|
29
29
|
export { PasswordManager, PasswordPolicyError, } from './src/password/password_manager.js';
|
|
30
30
|
export type { PasswordConfigInput, LegacyPasswordVerifier, PasswordVerifyResult, } from './src/password/password_manager.js';
|
|
@@ -125,6 +125,8 @@ export { resolveEffectiveBotProtection } from './src/host/bot_protection.js';
|
|
|
125
125
|
export type { BotProtectionSetting } from './src/host/bot_protection.js';
|
|
126
126
|
export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, resolveEffectiveAuthMethods, configLockedAuthMethods, } from './src/host/runtime_toggles.js';
|
|
127
127
|
export type { SettingKey, RegistrationSetting, RequireVerifiedEmailSetting, MaintenanceModeSetting, ResolvedMaintenanceMode, AuthMethodsSetting, ResolvedAuthMethods, AuthMethodsCapabilities, AuthMethodsConfigOverride, } from './src/host/runtime_toggles.js';
|
|
128
|
+
export { USER_LOGIN_METHOD_KEYS, normalizeUserLoginMethods, parseUserLoginMethodsPayload, resolveEffectiveUserLoginMethods, } from './src/host/user_login_methods.js';
|
|
129
|
+
export type { UserLoginMethodKey, UserLoginMethods, ResolvedUserLoginMethods, } from './src/host/user_login_methods.js';
|
|
128
130
|
export { resolveRegistration } from './src/define_config.js';
|
|
129
131
|
export type { RegistrationConfigInput, ResolvedRegistrationConfig, } from './src/define_config.js';
|
|
130
132
|
export { getAccountId, realAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
|
package/build/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { resolveTrustedDevices, isTrustedDeviceValid, buildTrustedDevicePayload,
|
|
|
14
14
|
export { resolveBotProtection, botProtectionApplies, extractBotToken, verifyBotProtection, guardBotProtection, DEFAULT_BOT_TOKEN_FIELDS, } from './src/host/bot_protection.js';
|
|
15
15
|
export { lucidAccountStore, appKeyEncrypter } from './src/accounts/lucid_account_store.js';
|
|
16
16
|
export { lucidStores } from './src/accounts/lucid_stores.js';
|
|
17
|
-
export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, supportsAccountStatus, supportsProfile, supportsMagicLink, supportsOtpLogin, supportsEmailVerificationStatus, supportsAccountDeletion, supportsAccountImport, } from './src/accounts/account_store.js';
|
|
17
|
+
export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, supportsAccountStatus, supportsProfile, supportsMagicLink, supportsOtpLogin, supportsEmailVerificationStatus, supportsAccountDeletion, supportsAccountImport, supportsLoginMethodsPreference, } from './src/accounts/account_store.js';
|
|
18
18
|
// Login por OTP (código digitável): config + helpers puros.
|
|
19
19
|
export { resolveOtpLoginConfig, generateOtpCode, evaluateLoginOtp, OTP_LOGIN_DEFAULTS, } from './src/host/otp_login.js';
|
|
20
20
|
// Gerência de senha: lazy rehash + legacy verifier, política e checagem de vazamento.
|
|
@@ -85,6 +85,8 @@ export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
|
85
85
|
export { resolveEffectiveBotProtection } from './src/host/bot_protection.js';
|
|
86
86
|
// Runtime toggles (registration, require_verified_email, maintenance_mode).
|
|
87
87
|
export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, resolveEffectiveAuthMethods, configLockedAuthMethods, } from './src/host/runtime_toggles.js';
|
|
88
|
+
// Tipos de login POR USUÁRIO (self-service no console de conta).
|
|
89
|
+
export { USER_LOGIN_METHOD_KEYS, normalizeUserLoginMethods, parseUserLoginMethodsPayload, resolveEffectiveUserLoginMethods, } from './src/host/user_login_methods.js';
|
|
88
90
|
export { resolveRegistration } from './src/define_config.js';
|
|
89
91
|
export { getAccountId, realAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
|
|
90
92
|
export { ACCOUNT_SESSION_KEY } from './src/host/account_session_key.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** DTO público da conta — o que o provider e os controllers enxergam. Nunca um model Lucid. */
|
|
2
|
+
import type { UserLoginMethods } from '../host/user_login_methods.js';
|
|
2
3
|
export interface AuthAccount {
|
|
3
4
|
id: string;
|
|
4
5
|
email: string;
|
|
@@ -211,6 +212,22 @@ export interface ProviderIdentitySummary {
|
|
|
211
212
|
providerUserId: string;
|
|
212
213
|
email?: string | null;
|
|
213
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Preferência POR USUÁRIO de tipos de login (self-service no console de conta).
|
|
217
|
+
* CAPACIDADE opcional, montada quando o model tem a coluna `login_methods`
|
|
218
|
+
* (JSONB) — hosts adotam por migração própria. `null` = sem preferência (herda
|
|
219
|
+
* os métodos globais efetivos). Ver `host/user_login_methods.ts` para o shape
|
|
220
|
+
* canônico (`UserLoginMethods`) e a semântica da interseção.
|
|
221
|
+
*/
|
|
222
|
+
export interface LoginMethodsPreferenceCapability {
|
|
223
|
+
/** Lê a preferência da conta; null quando não há preferência persistida. */
|
|
224
|
+
getLoginMethods(accountId: string): Promise<UserLoginMethods | null>;
|
|
225
|
+
/**
|
|
226
|
+
* Grava a preferência da conta. Objeto vazio ou null REMOVE a preferência
|
|
227
|
+
* (volta a herdar os globais). No-op se a conta não existe.
|
|
228
|
+
*/
|
|
229
|
+
setLoginMethods(accountId: string, methods: UserLoginMethods | null): Promise<void>;
|
|
230
|
+
}
|
|
214
231
|
/**
|
|
215
232
|
* MFA / TOTP. Stores sem suporte a MFA omitem a capacidade inteira; o interaction
|
|
216
233
|
* flow trata a ausência como "MFA desligado".
|
|
@@ -599,7 +616,7 @@ export type AccountStore = CoreAccountStore & {
|
|
|
599
616
|
* blocos `Partial<...>` de capacidades probáveis.
|
|
600
617
|
*/
|
|
601
618
|
readonly connectionName?: string;
|
|
602
|
-
} & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability & AccountStatusCapability & ProfileCapability & MagicLinkCapability & OtpLoginCapability & EmailVerificationStatusCapability & AccountDeletionCapability & AccountImportCapability & OrganizationsCapability & PasswordHistoryCapability & PasswordExpirationCapability>;
|
|
619
|
+
} & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability & AccountStatusCapability & ProfileCapability & MagicLinkCapability & OtpLoginCapability & EmailVerificationStatusCapability & AccountDeletionCapability & AccountImportCapability & OrganizationsCapability & PasswordHistoryCapability & PasswordExpirationCapability & LoginMethodsPreferenceCapability>;
|
|
603
620
|
/** Type guard: o store implementa a capacidade de MFA / TOTP. */
|
|
604
621
|
export declare function supportsMfa(store: AccountStore): store is AccountStore & MfaCapability;
|
|
605
622
|
/**
|
|
@@ -634,3 +651,5 @@ export declare function supportsAccountImport(store: AccountStore): store is Acc
|
|
|
634
651
|
export declare function supportsPasswordHistory(store: AccountStore): store is AccountStore & PasswordHistoryCapability;
|
|
635
652
|
/** Type guard: o store implementa expiração de senha (password_changed_at coluna). */
|
|
636
653
|
export declare function supportsPasswordExpiration(store: AccountStore): store is AccountStore & PasswordExpirationCapability;
|
|
654
|
+
/** Type guard: o store implementa a preferência por usuário de tipos de login. */
|
|
655
|
+
export declare function supportsLoginMethodsPreference(store: AccountStore): store is AccountStore & LoginMethodsPreferenceCapability;
|
|
@@ -63,3 +63,7 @@ export function supportsPasswordHistory(store) {
|
|
|
63
63
|
export function supportsPasswordExpiration(store) {
|
|
64
64
|
return typeof store.getPasswordChangedAt === 'function';
|
|
65
65
|
}
|
|
66
|
+
/** Type guard: o store implementa a preferência por usuário de tipos de login. */
|
|
67
|
+
export function supportsLoginMethodsPreference(store) {
|
|
68
|
+
return typeof store.getLoginMethods === 'function';
|
|
69
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server';
|
|
2
2
|
import { PasswordManager } from '../password/password_manager.js';
|
|
3
3
|
import { buildCore } from './lucid_store/core.js';
|
|
4
|
+
import { buildLoginMethods, supportsLoginMethodsColumn } from './lucid_store/login_methods.js';
|
|
4
5
|
import { buildMfa } from './lucid_store/mfa.js';
|
|
5
6
|
import { buildOrganizations } from './lucid_store/organizations.js';
|
|
6
7
|
import { buildPasswordExpiration, buildPasswordHistory } from './lucid_store/password_hygiene.js';
|
|
@@ -212,6 +213,9 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
212
213
|
...buildDeletion(ctx),
|
|
213
214
|
// Expiração de senha: só quando o model tem a coluna `password_changed_at`.
|
|
214
215
|
...(hasColumn(Model, 'passwordChangedAt') ? buildPasswordExpiration(ctx) : {}),
|
|
216
|
+
// Preferência por usuário de tipos de login: só quando o model tem a coluna
|
|
217
|
+
// `login_methods` (JSONB). Sem a coluna → capacidade ausente (feature no-op).
|
|
218
|
+
...(supportsLoginMethodsColumn(Model) ? buildLoginMethods(ctx) : {}),
|
|
215
219
|
// Organizations (multi-tenancy): só quando os três models foram fornecidos.
|
|
216
220
|
...(OrgModels
|
|
217
221
|
? buildOrganizations({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoginMethodsPreferenceCapability } from '../account_store.js';
|
|
2
|
+
import type { LucidStoreContext } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* Preferência por usuário de tipos de login sobre a coluna `login_methods`
|
|
5
|
+
* (JSONB) do model. Só deve ser montada quando a coluna existe
|
|
6
|
+
* ({@link hasColumn}) — caso contrário a capacidade fica ausente e a feature
|
|
7
|
+
* degrada para no-op (hosts adotam por migração própria).
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildLoginMethods(ctx: LucidStoreContext): LoginMethodsPreferenceCapability;
|
|
10
|
+
/** Indica se o model suporta a preferência de tipos de login (coluna presente). */
|
|
11
|
+
export declare function supportsLoginMethodsColumn(Model: any): boolean;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { normalizeUserLoginMethods } from '../../host/user_login_methods.js';
|
|
2
|
+
import { hasColumn } from './status_profile.js';
|
|
3
|
+
/**
|
|
4
|
+
* Preferência por usuário de tipos de login sobre a coluna `login_methods`
|
|
5
|
+
* (JSONB) do model. Só deve ser montada quando a coluna existe
|
|
6
|
+
* ({@link hasColumn}) — caso contrário a capacidade fica ausente e a feature
|
|
7
|
+
* degrada para no-op (hosts adotam por migração própria).
|
|
8
|
+
*/
|
|
9
|
+
export function buildLoginMethods(ctx) {
|
|
10
|
+
const { Model } = ctx;
|
|
11
|
+
return {
|
|
12
|
+
async getLoginMethods(accountId) {
|
|
13
|
+
const row = await Model.find(accountId);
|
|
14
|
+
if (!row)
|
|
15
|
+
return null;
|
|
16
|
+
return normalizeUserLoginMethods(row.loginMethods);
|
|
17
|
+
},
|
|
18
|
+
async setLoginMethods(accountId, methods) {
|
|
19
|
+
const row = await Model.find(accountId);
|
|
20
|
+
if (!row)
|
|
21
|
+
return;
|
|
22
|
+
// Objeto vazio/null → grava null (sem preferência = herda globais).
|
|
23
|
+
const value = methods && Object.keys(methods).length > 0 ? normalizeUserLoginMethods(methods) : null;
|
|
24
|
+
row.loginMethods = value;
|
|
25
|
+
await row.save();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Indica se o model suporta a preferência de tipos de login (coluna presente). */
|
|
30
|
+
export function supportsLoginMethodsColumn(Model) {
|
|
31
|
+
return hasColumn(Model, 'loginMethods');
|
|
32
|
+
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import '../augmentations.js';
|
|
31
31
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
32
32
|
export default class AccountApiController {
|
|
33
|
+
#private;
|
|
33
34
|
/** Perfil + flags do usuário logado. */
|
|
34
35
|
me(ctx: HttpContext): Promise<void | {
|
|
35
36
|
id: any;
|
|
@@ -171,6 +172,43 @@ export default class AccountApiController {
|
|
|
171
172
|
available: any;
|
|
172
173
|
};
|
|
173
174
|
}>;
|
|
175
|
+
/**
|
|
176
|
+
* Preferência POR USUÁRIO de tipos de login + o catálogo do que está
|
|
177
|
+
* disponível/travado. A interseção com os métodos globais efetivos é feita
|
|
178
|
+
* AQUI (a UI mostra o estado final; o enforcement no login reusa o resolver).
|
|
179
|
+
*/
|
|
180
|
+
getLoginMethods(ctx: HttpContext): Promise<{
|
|
181
|
+
supported: false;
|
|
182
|
+
methods: null;
|
|
183
|
+
available: null;
|
|
184
|
+
locked: null;
|
|
185
|
+
} | {
|
|
186
|
+
supported: true;
|
|
187
|
+
methods: any;
|
|
188
|
+
available: {
|
|
189
|
+
password: boolean;
|
|
190
|
+
magicLink: boolean;
|
|
191
|
+
passkey: boolean;
|
|
192
|
+
social: string[];
|
|
193
|
+
forgotPassword: boolean;
|
|
194
|
+
};
|
|
195
|
+
locked: {
|
|
196
|
+
password: boolean;
|
|
197
|
+
magicLink: boolean;
|
|
198
|
+
passkey: boolean;
|
|
199
|
+
social: boolean;
|
|
200
|
+
};
|
|
201
|
+
}>;
|
|
202
|
+
/** Grava a preferência de tipos de login do usuário logado. */
|
|
203
|
+
updateLoginMethods(ctx: HttpContext): Promise<void | {
|
|
204
|
+
ok: boolean;
|
|
205
|
+
methods: {
|
|
206
|
+
password?: boolean;
|
|
207
|
+
magicLink?: boolean;
|
|
208
|
+
passkey?: boolean;
|
|
209
|
+
social?: boolean;
|
|
210
|
+
};
|
|
211
|
+
}>;
|
|
174
212
|
/** Lista passkeys do usuário logado. */
|
|
175
213
|
listPasskeys(ctx: HttpContext): Promise<{
|
|
176
214
|
supported: boolean;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* GET /account/api/orgs/invitations → convites pendentes
|
|
29
29
|
*/
|
|
30
30
|
import '../augmentations.js';
|
|
31
|
-
import { supportsAccountSecurity, supportsOrganizations, supportsPasskeys, supportsProfile, } from '../../accounts/account_store.js';
|
|
31
|
+
import { supportsAccountSecurity, supportsLoginMethodsPreference, supportsMagicLink, supportsOrganizations, supportsPasskeys, supportsProfile, } from '../../accounts/account_store.js';
|
|
32
32
|
import { PasswordPolicyError } from '../../password/password_manager.js';
|
|
33
33
|
import { accountPath } from '../account_paths.js';
|
|
34
34
|
import { ACCOUNT_SESSION_KEY } from '../account_session_key.js';
|
|
@@ -39,11 +39,12 @@ import { AvatarUploadError, isAvatarUploadSupported, storeAvatar } from '../avat
|
|
|
39
39
|
import { sendEmailChangeConfirmationEmail, sendEmailChangeNoticeEmail } from '../default_mailer.js';
|
|
40
40
|
import { translate } from '../i18n.js';
|
|
41
41
|
import { authkitOrigin } from '../origin.js';
|
|
42
|
-
import { resolveRuntimeSettings } from '../runtime_settings.js';
|
|
43
|
-
import { resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '../runtime_toggles.js';
|
|
42
|
+
import { resolveRuntimeSettings, resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
43
|
+
import { configLockedAuthMethods, resolveEffectiveAuthMethods, resolveEffectiveEmailChange, resolveEffectivePasswordHistory, } from '../runtime_toggles.js';
|
|
44
44
|
import { dispatchSecurityNotice } from '../security_notice_service.js';
|
|
45
45
|
import { enrichSessionsWithContext } from '../session_context.js';
|
|
46
46
|
import { SUDO_MODE_DEFAULTS, isSudoActive, requireSudo, resolveEffectiveSudoMode, } from '../sudo_mode.js';
|
|
47
|
+
import { parseUserLoginMethodsPayload, resolveEffectiveUserLoginMethods, } from '../user_login_methods.js';
|
|
47
48
|
import { changeEmailValidator, changePasswordValidator, updateProfileValidator, } from '../validators.js';
|
|
48
49
|
// ---------------------------------------------------------------------------
|
|
49
50
|
// Helpers (shared with existing controllers — kept local to avoid coupling)
|
|
@@ -641,6 +642,86 @@ export default class AccountApiController {
|
|
|
641
642
|
recovery: { available: enabled },
|
|
642
643
|
};
|
|
643
644
|
}
|
|
645
|
+
// ─── GET /account/api/login-methods ─────────────────────────────────────
|
|
646
|
+
/**
|
|
647
|
+
* Preferência POR USUÁRIO de tipos de login + o catálogo do que está
|
|
648
|
+
* disponível/travado. A interseção com os métodos globais efetivos é feita
|
|
649
|
+
* AQUI (a UI mostra o estado final; o enforcement no login reusa o resolver).
|
|
650
|
+
*/
|
|
651
|
+
async getLoginMethods(ctx) {
|
|
652
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
653
|
+
const cfg = service.config;
|
|
654
|
+
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
655
|
+
const supported = supportsLoginMethodsPreference(cfg.accountStore);
|
|
656
|
+
if (!supported) {
|
|
657
|
+
return { supported: false, methods: null, available: null, locked: null };
|
|
658
|
+
}
|
|
659
|
+
const pref = await cfg.accountStore.getLoginMethods(userId);
|
|
660
|
+
const { resolved: global, locked: cfgLocked } = await this.#resolveGlobalAuthMethods(ctx, cfg);
|
|
661
|
+
const effective = resolveEffectiveUserLoginMethods(global, pref);
|
|
662
|
+
return {
|
|
663
|
+
supported: true,
|
|
664
|
+
// Preferência crua do usuário (o que ELE escolheu; null = sem preferência).
|
|
665
|
+
methods: pref ?? {},
|
|
666
|
+
// Estado final por método (global ∩ preferência) — o que a tela de login mostra.
|
|
667
|
+
available: {
|
|
668
|
+
password: effective.password,
|
|
669
|
+
magicLink: effective.magicLink,
|
|
670
|
+
passkey: effective.passkey,
|
|
671
|
+
social: effective.social,
|
|
672
|
+
forgotPassword: effective.forgotPassword,
|
|
673
|
+
},
|
|
674
|
+
// Métodos que o usuário NÃO pode controlar (globalmente off ou pin de config).
|
|
675
|
+
// Pin de config: `cfgLocked` lista as chaves fixadas no arquivo de config.
|
|
676
|
+
locked: {
|
|
677
|
+
password: !global.password || cfgLocked.includes('password'),
|
|
678
|
+
magicLink: !global.magicLink || cfgLocked.includes('magicLink'),
|
|
679
|
+
passkey: !global.passkey || cfgLocked.includes('passkey'),
|
|
680
|
+
social: global.social.length === 0,
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
// ─── PUT /account/api/login-methods ─────────────────────────────────────
|
|
685
|
+
/** Grava a preferência de tipos de login do usuário logado. */
|
|
686
|
+
async updateLoginMethods(ctx) {
|
|
687
|
+
const service = await ctx.containerResolver.make('authkit.server');
|
|
688
|
+
const cfg = service.config;
|
|
689
|
+
const userId = ctx.session.get(ACCOUNT_SESSION_KEY);
|
|
690
|
+
if (!supportsLoginMethodsPreference(cfg.accountStore)) {
|
|
691
|
+
return ctx.response.notFound(apiErr('not_supported', 'Login methods preference not supported.'));
|
|
692
|
+
}
|
|
693
|
+
const parsed = parseUserLoginMethodsPayload(ctx.request.body());
|
|
694
|
+
if (!parsed.ok) {
|
|
695
|
+
return ctx.response.badRequest(apiErr(parsed.error, 'Invalid login methods payload.'));
|
|
696
|
+
}
|
|
697
|
+
// All-off guard server-side: a preferência não pode zerar todos os métodos
|
|
698
|
+
// GLOBAIS disponíveis. Um `false` que deixaria zero métodos é recusado.
|
|
699
|
+
const { resolved: global } = await this.#resolveGlobalAuthMethods(ctx, cfg);
|
|
700
|
+
const value = { ...parsed.value };
|
|
701
|
+
const wouldBeAllOff = (value.password === false || !global.password) &&
|
|
702
|
+
(value.magicLink === false || !global.magicLink) &&
|
|
703
|
+
(value.passkey === false || !global.passkey) &&
|
|
704
|
+
(value.social === false || global.social.length === 0);
|
|
705
|
+
if (wouldBeAllOff) {
|
|
706
|
+
return ctx.response.badRequest(apiErr('all_methods_off', 'Cannot disable every login method.'));
|
|
707
|
+
}
|
|
708
|
+
await cfg.accountStore.setLoginMethods(userId, Object.keys(value).length > 0 ? value : null);
|
|
709
|
+
return { ok: true, methods: value };
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Métodos globais efetivos + pins de config, compartilhados pelos handlers
|
|
713
|
+
* acima. Fail-safe: erro → defaults conservadores.
|
|
714
|
+
*/
|
|
715
|
+
async #resolveGlobalAuthMethods(ctx, cfg) {
|
|
716
|
+
const settings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
717
|
+
const resolved = await resolveEffectiveAuthMethods(settings, {
|
|
718
|
+
configuredSocialProviders: cfg.social?.providers ?? [],
|
|
719
|
+
magicLinkCapable: cfg.passwordless?.magicLink && supportsMagicLink(cfg.accountStore),
|
|
720
|
+
passkeyCapable: supportsPasskeys(cfg.accountStore),
|
|
721
|
+
configOverrides: cfg.authMethods,
|
|
722
|
+
});
|
|
723
|
+
return { resolved, locked: configLockedAuthMethods(cfg.authMethods) };
|
|
724
|
+
}
|
|
644
725
|
// ─── GET /account/api/passkeys ──────────────────────────────────────────
|
|
645
726
|
/** Lista passkeys do usuário logado. */
|
|
646
727
|
async listPasskeys(ctx) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
|
-
import { supportsMagicLink, supportsOtpLogin, supportsPasskeys, } from '../../accounts/account_store.js';
|
|
2
|
+
import { supportsLoginMethodsPreference, supportsMagicLink, supportsOtpLogin, supportsPasskeys, } from '../../accounts/account_store.js';
|
|
3
3
|
import { AdminSessionsService } from '../admin_sessions_service.js';
|
|
4
4
|
import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
|
|
5
5
|
import { brandFor, isFirstParty } from '../branding.js';
|
|
@@ -14,6 +14,7 @@ import { createOtpLockout, generateOtpUnlockToken, rawToDbOtpUnlockToken, resolv
|
|
|
14
14
|
import { resolveRuntimeSettingsOrNoop } from '../runtime_settings.js';
|
|
15
15
|
import { resolveEffectiveAuthMethods, resolveEffectiveMaintenanceMode, resolveEffectiveRegistration, resolveEffectiveSessionPolicy, } from '../runtime_toggles.js';
|
|
16
16
|
import { TRUSTED_DEVICE_COOKIE, buildTrustedDevicePayload, isTrustedDeviceValid, } from '../trusted_device.js';
|
|
17
|
+
import { normalizeUserLoginMethods, resolveEffectiveUserLoginMethods, } from '../user_login_methods.js';
|
|
17
18
|
/**
|
|
18
19
|
* Chave i18n do erro de status de conta (disabled/expired) compartilhada pelos
|
|
19
20
|
* fluxos passwordless (magic link, OTP, passkey).
|
|
@@ -73,6 +74,44 @@ export default class AuthInteractionController {
|
|
|
73
74
|
otpEnabled,
|
|
74
75
|
};
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Métodos de login com a preferência POR USUÁRIO aplicada (interseção global ×
|
|
79
|
+
* `login_methods` da conta). Usado em TODO render/POST que já conhece o email
|
|
80
|
+
* (passo 2+ do login) — o passo 1 (identifier) não tem usuário, então usa só o
|
|
81
|
+
* global via {@link #loginMethods}.
|
|
82
|
+
*
|
|
83
|
+
* Fail-safe: store sem a capacidade, conta inexistente ou qualquer erro →
|
|
84
|
+
* métodos globais sem alteração (a preferência nunca derruba o login).
|
|
85
|
+
*/
|
|
86
|
+
async #userScopedLoginMethods(ctx, cfg, email, runtimeSettings) {
|
|
87
|
+
const base = await this.#loginMethods(ctx, cfg, runtimeSettings);
|
|
88
|
+
if (!email)
|
|
89
|
+
return base;
|
|
90
|
+
try {
|
|
91
|
+
if (!supportsLoginMethodsPreference(cfg.accountStore))
|
|
92
|
+
return base;
|
|
93
|
+
const acc = await cfg.accountStore.findByEmail(email);
|
|
94
|
+
if (!acc)
|
|
95
|
+
return base;
|
|
96
|
+
const pref = await cfg.accountStore.getLoginMethods(acc.id);
|
|
97
|
+
const normalized = normalizeUserLoginMethods(pref);
|
|
98
|
+
if (!normalized)
|
|
99
|
+
return base;
|
|
100
|
+
const scoped = resolveEffectiveUserLoginMethods(base.authMethods, normalized);
|
|
101
|
+
const magicLinkCapableConfig = cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore);
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
authMethods: scoped,
|
|
105
|
+
magicLinkAvailable: scoped.magicLink && magicLinkCapableConfig,
|
|
106
|
+
// OTP é extensão do magic link — desligar magic link desliga o código.
|
|
107
|
+
otpEnabled: base.otpEnabled && scoped.magicLink,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Preferência nunca derruba o login.
|
|
112
|
+
return base;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
76
115
|
async show(ctx) {
|
|
77
116
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
78
117
|
const cfg = service.config;
|
|
@@ -129,7 +168,11 @@ export default class AuthInteractionController {
|
|
|
129
168
|
// `authMethods`, `magicLinkAvailable` e `otpEnabled` saiam POR CONSTRUÇÃO
|
|
130
169
|
// (o seletor choose-first precisa de `otpEnabled` para oferecer "código").
|
|
131
170
|
// Passa o runtimeSettings já resolvido para maintenance (sem 2ª resolução).
|
|
132
|
-
|
|
171
|
+
// Com email na sessão (passo 2+), aplica a preferência POR USUÁRIO — os
|
|
172
|
+
// métodos que o dono da conta desligou não aparecem nem aqui nem nos POSTs.
|
|
173
|
+
const loginMethods = email
|
|
174
|
+
? await this.#userScopedLoginMethods(ctx, cfg, email, runtimeSettingsForMaintenance)
|
|
175
|
+
: await this.#loginMethods(ctx, cfg, runtimeSettingsForMaintenance);
|
|
133
176
|
const authMethods = loginMethods.authMethods;
|
|
134
177
|
if (!email) {
|
|
135
178
|
// Step 1: identifier (email only).
|
|
@@ -215,7 +258,7 @@ export default class AuthInteractionController {
|
|
|
215
258
|
? { fullName: found.name ?? null, globalRoles: found.globalRoles ?? [] }
|
|
216
259
|
: null;
|
|
217
260
|
return render(ctx, 'login', {
|
|
218
|
-
...(await this.#
|
|
261
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, email, runtimeSettings)),
|
|
219
262
|
uid: ctx.request.param('uid'),
|
|
220
263
|
csrfToken: ctx.request.csrfToken,
|
|
221
264
|
step: 'password',
|
|
@@ -226,6 +269,35 @@ export default class AuthInteractionController {
|
|
|
226
269
|
botProtection: effectiveBotLogin?.widget,
|
|
227
270
|
});
|
|
228
271
|
}
|
|
272
|
+
// Preferência por usuário: senha desligada para ESTA conta → recusa ANTES de
|
|
273
|
+
// verificar credenciais (mesma tela/erro de credencial inválida — não vaza
|
|
274
|
+
// se a conta existe nem qual preferência está gravada).
|
|
275
|
+
const userMethodsForLogin = await this.#userScopedLoginMethods(ctx, cfg, email, runtimeSettings);
|
|
276
|
+
if (!userMethodsForLogin.authMethods.password) {
|
|
277
|
+
const found = await cfg.accountStore.findByEmail(email);
|
|
278
|
+
const account = found
|
|
279
|
+
? { fullName: found.name ?? null, globalRoles: found.globalRoles ?? [] }
|
|
280
|
+
: null;
|
|
281
|
+
await cfg.audit?.record({
|
|
282
|
+
type: 'login.failure',
|
|
283
|
+
ip,
|
|
284
|
+
clientId,
|
|
285
|
+
metadata: { stage: 'password', reason: 'method_disabled_by_user' },
|
|
286
|
+
});
|
|
287
|
+
return render(ctx, 'login', {
|
|
288
|
+
...userMethodsForLogin,
|
|
289
|
+
uid: ctx.request.param('uid'),
|
|
290
|
+
csrfToken: ctx.request.csrfToken,
|
|
291
|
+
step: 'password',
|
|
292
|
+
email,
|
|
293
|
+
account,
|
|
294
|
+
error: translate(cfg.messages, 'errors.invalid_credentials'),
|
|
295
|
+
brand,
|
|
296
|
+
botProtection: effectiveBotLogin?.on.includes('login')
|
|
297
|
+
? effectiveBotLogin.widget
|
|
298
|
+
: undefined,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
229
301
|
// Verificamos as credenciais ANTES de finalizar a interaction, porque com MFA
|
|
230
302
|
// ligado precisamos exigir o 2º fator e NÃO podemos chamar interactionFinished
|
|
231
303
|
// ainda. A sequência verificação + lockout + auditoria de falha é centralizada
|
|
@@ -260,7 +332,7 @@ export default class AuthInteractionController {
|
|
|
260
332
|
? { fullName: found.name ?? null, globalRoles: found.globalRoles ?? [] }
|
|
261
333
|
: null;
|
|
262
334
|
return render(ctx, 'login', {
|
|
263
|
-
...
|
|
335
|
+
...userMethodsForLogin,
|
|
264
336
|
uid: ctx.request.param('uid'),
|
|
265
337
|
csrfToken: ctx.request.csrfToken,
|
|
266
338
|
step: 'password',
|
|
@@ -633,6 +705,24 @@ export default class AuthInteractionController {
|
|
|
633
705
|
// o e-mail e a tela mostrem SÓ aquele método. Ausente/ inválido = both (histórico).
|
|
634
706
|
// NÃO condiciona a emissão de token — os dois continuam saindo co-locados.
|
|
635
707
|
const channel = normalizeLoginChannel(ctx.request.input('channel'));
|
|
708
|
+
// Preferência por usuário: magic link desligado para ESTA conta → não emite
|
|
709
|
+
// token (resposta uniforme "enviado", sem vazar a preferência).
|
|
710
|
+
const userScopedMethods = email && cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore)
|
|
711
|
+
? await this.#userScopedLoginMethods(ctx, cfg, email)
|
|
712
|
+
: null;
|
|
713
|
+
if (userScopedMethods && !userScopedMethods.authMethods.magicLink) {
|
|
714
|
+
return render(ctx, 'login', {
|
|
715
|
+
...userScopedMethods,
|
|
716
|
+
uid,
|
|
717
|
+
csrfToken: ctx.request.csrfToken,
|
|
718
|
+
step: 'password',
|
|
719
|
+
email,
|
|
720
|
+
account: null,
|
|
721
|
+
brand,
|
|
722
|
+
magicLinkSent: true,
|
|
723
|
+
magicChannel: 'none',
|
|
724
|
+
});
|
|
725
|
+
}
|
|
636
726
|
if (cfg.passwordless.magicLink && supportsMagicLink(cfg.accountStore) && email) {
|
|
637
727
|
const ip = ctx.request.ip?.() ?? null;
|
|
638
728
|
const clientId = details.params.client_id ?? null;
|
|
@@ -676,7 +766,9 @@ export default class AuthInteractionController {
|
|
|
676
766
|
// `otpEnabled` vem do spread de `#loginMethods` (mesmo valor do `otpEnabled`
|
|
677
767
|
// local usado acima para decidir a emissão) — não re-declarado aqui.
|
|
678
768
|
return render(ctx, 'login', {
|
|
679
|
-
...(
|
|
769
|
+
...(email
|
|
770
|
+
? await this.#userScopedLoginMethods(ctx, cfg, email)
|
|
771
|
+
: await this.#loginMethods(ctx, cfg)),
|
|
680
772
|
uid,
|
|
681
773
|
csrfToken: ctx.request.csrfToken,
|
|
682
774
|
step: 'password',
|
|
@@ -715,6 +807,31 @@ export default class AuthInteractionController {
|
|
|
715
807
|
});
|
|
716
808
|
return ctx.response.redirect(`/auth/interaction/${uid}`);
|
|
717
809
|
}
|
|
810
|
+
// Preferência por usuário: magic link desligado para esta conta (p. ex. o
|
|
811
|
+
// token foi emitido antes de o usuário desligar o método) → não materializa
|
|
812
|
+
// a sessão; volta ao login com erro genérico (não vaza a preferência).
|
|
813
|
+
const magicScoped = await this.#userScopedLoginMethods(ctx, cfg, acc.email);
|
|
814
|
+
if (!magicScoped.authMethods.magicLink) {
|
|
815
|
+
await cfg.audit?.record({
|
|
816
|
+
type: 'login.failure',
|
|
817
|
+
accountId: acc.id,
|
|
818
|
+
email: acc.email,
|
|
819
|
+
ip,
|
|
820
|
+
clientId,
|
|
821
|
+
metadata: { stage: 'magic_link', reason: 'method_disabled_by_user' },
|
|
822
|
+
});
|
|
823
|
+
const render = cfg.render;
|
|
824
|
+
return render(ctx, 'login', {
|
|
825
|
+
...magicScoped,
|
|
826
|
+
uid,
|
|
827
|
+
csrfToken: ctx.request.csrfToken,
|
|
828
|
+
step: 'password',
|
|
829
|
+
email: acc.email,
|
|
830
|
+
account: null,
|
|
831
|
+
brand: brandFor(cfg.branding, clientId ?? undefined, undefined),
|
|
832
|
+
error: translate(cfg.messages, 'errors.invalid_credentials'),
|
|
833
|
+
});
|
|
834
|
+
}
|
|
718
835
|
// E-mail não verificado (LGPD/compliance): mesmo com o link válido, não
|
|
719
836
|
// materializa a sessão se a política exige verificação. Volta ao login com erro.
|
|
720
837
|
const magicLinkRuntimeSettings = await resolveRuntimeSettingsOrNoop(ctx);
|
|
@@ -729,7 +846,7 @@ export default class AuthInteractionController {
|
|
|
729
846
|
});
|
|
730
847
|
const render = cfg.render;
|
|
731
848
|
return render(ctx, 'login', {
|
|
732
|
-
...(await this.#
|
|
849
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, acc.email)),
|
|
733
850
|
uid,
|
|
734
851
|
csrfToken: ctx.request.csrfToken,
|
|
735
852
|
step: 'password',
|
|
@@ -762,7 +879,7 @@ export default class AuthInteractionController {
|
|
|
762
879
|
if (!magicLinkStatusGate.allowed) {
|
|
763
880
|
const render = cfg.render;
|
|
764
881
|
return render(ctx, 'login', {
|
|
765
|
-
...(await this.#
|
|
882
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, acc.email)),
|
|
766
883
|
uid,
|
|
767
884
|
csrfToken: ctx.request.csrfToken,
|
|
768
885
|
step: 'password',
|
|
@@ -814,10 +931,10 @@ export default class AuthInteractionController {
|
|
|
814
931
|
maxAttempts: cfg.login.otp.maxAttempts,
|
|
815
932
|
});
|
|
816
933
|
// Re-render da tela "link enviado" com o campo de código + erro localizado.
|
|
817
|
-
// `otpEnabled` vem do spread
|
|
818
|
-
//
|
|
934
|
+
// `otpEnabled` vem do spread (scoped — OTP é extensão do magic link, e a
|
|
935
|
+
// preferência do usuário que desligou magic link já foi barrada acima).
|
|
819
936
|
const renderOtpError = async (messageKey) => render(ctx, 'login', {
|
|
820
|
-
...(await this.#
|
|
937
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, email)),
|
|
821
938
|
uid,
|
|
822
939
|
csrfToken: ctx.request.csrfToken,
|
|
823
940
|
step: 'password',
|
|
@@ -842,7 +959,7 @@ export default class AuthInteractionController {
|
|
|
842
959
|
metadata: { stage: 'otp', reason: 'unverified' },
|
|
843
960
|
});
|
|
844
961
|
return render(ctx, 'login', {
|
|
845
|
-
...(await this.#
|
|
962
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, result.account.email)),
|
|
846
963
|
uid,
|
|
847
964
|
csrfToken: ctx.request.csrfToken,
|
|
848
965
|
step: 'password',
|
|
@@ -868,7 +985,7 @@ export default class AuthInteractionController {
|
|
|
868
985
|
});
|
|
869
986
|
if (!otpStatusGate.allowed) {
|
|
870
987
|
return render(ctx, 'login', {
|
|
871
|
-
...(await this.#
|
|
988
|
+
...(await this.#userScopedLoginMethods(ctx, cfg, result.account.email)),
|
|
872
989
|
uid,
|
|
873
990
|
csrfToken: ctx.request.csrfToken,
|
|
874
991
|
step: 'password',
|
|
@@ -1033,6 +1150,36 @@ export default class AuthInteractionController {
|
|
|
1033
1150
|
false)
|
|
1034
1151
|
: false;
|
|
1035
1152
|
ctx.session.forget(PASSKEY_AUTH_CHALLENGE_KEY);
|
|
1153
|
+
// Preferência por usuário: passkey desligada para esta conta → recusa a
|
|
1154
|
+
// assertion. Checamos DEPOIS de `ok` para não revelar a preferência no
|
|
1155
|
+
// timing de uma assertion inválida. Aqui vale SÓ a preferência explícita do
|
|
1156
|
+
// usuário (`pref.passkey === false`): os switches globais de passkey já são
|
|
1157
|
+
// aplicados na RENDERIZAÇÃO do botão/passkey-first (e o desafio MFA usa
|
|
1158
|
+
// passkey como 2º fator independente do passkey-first).
|
|
1159
|
+
if (ok && accountId && supportsLoginMethodsPreference(cfg.accountStore)) {
|
|
1160
|
+
const prefAccount = await cfg.accountStore.findById(accountId);
|
|
1161
|
+
const pref = prefAccount
|
|
1162
|
+
? normalizeUserLoginMethods(await cfg.accountStore.getLoginMethods(prefAccount.id))
|
|
1163
|
+
: null;
|
|
1164
|
+
if (pref?.passkey === false) {
|
|
1165
|
+
await cfg.audit?.record({
|
|
1166
|
+
type: 'login.failure',
|
|
1167
|
+
accountId,
|
|
1168
|
+
ip,
|
|
1169
|
+
clientId,
|
|
1170
|
+
metadata: { stage: 'mfa', method: 'webauthn', reason: 'method_disabled_by_user' },
|
|
1171
|
+
});
|
|
1172
|
+
return render(ctx, 'mfa-challenge', {
|
|
1173
|
+
uid: ctx.request.param('uid'),
|
|
1174
|
+
csrfToken: ctx.request.csrfToken,
|
|
1175
|
+
error: translate(cfg.messages, 'mfa_challenge.passkey_error'),
|
|
1176
|
+
brand,
|
|
1177
|
+
passkeyAvailable: false,
|
|
1178
|
+
trustedDevicesEnabled: cfg.trustedDevices.enabled,
|
|
1179
|
+
trustedDeviceDays: cfg.trustedDevices.days,
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1036
1183
|
if (!ok) {
|
|
1037
1184
|
await cfg.audit?.record({
|
|
1038
1185
|
type: 'login.failure',
|