@adonis-agora/authkit-server 0.58.3 → 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 +21 -2
- 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/audit/audit_sink.d.ts +10 -0
- package/build/src/doctor/checks.d.ts +13 -0
- package/build/src/doctor/checks.js +79 -0
- package/build/src/events/dispatcher.d.ts +7 -3
- package/build/src/events/dispatcher.js +9 -3
- 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/admin_api/admin_orgs_service.js +8 -0
- package/build/src/host/controllers/account_orgs_controller.js +13 -2
- 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 +3 -3
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';
|
|
@@ -165,7 +166,11 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
165
166
|
return encrypter.decrypt(stored);
|
|
166
167
|
},
|
|
167
168
|
toAccount: (row) => ({
|
|
168
|
-
id
|
|
169
|
+
// `String(...)`: o `id` do AuthAccount vira o claim `sub`, que a spec OIDC
|
|
170
|
+
// exige que seja string. Tabelas adotadas em brownfield quase sempre têm
|
|
171
|
+
// `id` INTEGER auto-increment, e sem a coerção o número vazava para o
|
|
172
|
+
// token (e para todo call-site tipado como string).
|
|
173
|
+
id: String(row.id),
|
|
169
174
|
email: row.email,
|
|
170
175
|
globalRoles: row.globalRoles ?? [],
|
|
171
176
|
name: row.fullName ?? undefined,
|
|
@@ -208,6 +213,9 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
208
213
|
...buildDeletion(ctx),
|
|
209
214
|
// Expiração de senha: só quando o model tem a coluna `password_changed_at`.
|
|
210
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) : {}),
|
|
211
219
|
// Organizations (multi-tenancy): só quando os três models foram fornecidos.
|
|
212
220
|
...(OrgModels
|
|
213
221
|
? buildOrganizations({
|
|
@@ -251,6 +259,16 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
251
259
|
*/
|
|
252
260
|
__mfaIssuer: mfaIssuer,
|
|
253
261
|
__webauthn: options.webauthn,
|
|
262
|
+
/**
|
|
263
|
+
* O model Lucid por trás deste store — exposto para o `authkit:doctor`
|
|
264
|
+
* inspecionar `$columnsDefinitions` e avisar sobre colunas ausentes ANTES
|
|
265
|
+
* que o fluxo correspondente quebre em produção (ver
|
|
266
|
+
* `doctor/checks.ts#checkAccountStoreColumns`). Importa sobretudo em
|
|
267
|
+
* adoção brownfield, onde o model aponta para uma tabela `users` que já
|
|
268
|
+
* existia e não passou pelas migrations do authkit.
|
|
269
|
+
* NÃO faz parte do contrato AccountStore.
|
|
270
|
+
*/
|
|
271
|
+
__model: Model,
|
|
254
272
|
};
|
|
255
273
|
// Histórico de senhas: capability-probed via tabela `auth_password_history`.
|
|
256
274
|
// A versão síncrona não pode fazer o probe de DB, então a capability fica
|
|
@@ -287,7 +305,8 @@ export async function lucidAccountStoreAsync(Model, options = {}) {
|
|
|
287
305
|
sealSecret: (s) => s,
|
|
288
306
|
openSecret: (s) => s ?? null,
|
|
289
307
|
toAccount: (row) => ({
|
|
290
|
-
|
|
308
|
+
// Ver a nota em `toAccount` acima: `sub` precisa ser string.
|
|
309
|
+
id: String(row.id),
|
|
291
310
|
email: row.email,
|
|
292
311
|
globalRoles: row.globalRoles ?? [],
|
|
293
312
|
name: row.fullName ?? undefined,
|
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,16 @@ export interface AuditEvent {
|
|
|
21
21
|
clientId?: string | null;
|
|
22
22
|
/** Impersonation: quem agiu (o admin). */
|
|
23
23
|
actorId?: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Organização (tenant) a que o evento pertence, quando houver. Campo de
|
|
26
|
+
* PRIMEIRA CLASSE — e não uma chave de `metadata` — de propósito: `metadata`
|
|
27
|
+
* é livre e por isso é DROPADO na projeção que vai para o barramento de
|
|
28
|
+
* diagnostics (ver `redactAuditEventForDiagnostics`), enquanto os ids internos
|
|
29
|
+
* opacos (`accountId`/`actorId`/`clientId`/`orgId`) são preservados. É esse
|
|
30
|
+
* campo que permite a um consumidor do barramento — p.ex. o provisioning do
|
|
31
|
+
* `@adonis-agora/authz` — saber QUAL tenant provisionar sem receber PII.
|
|
32
|
+
*/
|
|
33
|
+
orgId?: string | null;
|
|
24
34
|
ip?: string | null;
|
|
25
35
|
metadata?: Record<string, unknown>;
|
|
26
36
|
}
|
|
@@ -57,6 +57,19 @@ export declare function checkClients(input: DoctorInput): Finding;
|
|
|
57
57
|
export declare function checkAdapterVolatility(input: DoctorInput): Finding | null;
|
|
58
58
|
/** accountStore presente + quais capacidades implementa. */
|
|
59
59
|
export declare function checkAccountStore(input: DoctorInput): Finding[];
|
|
60
|
+
/**
|
|
61
|
+
* Valida que o model por trás do account store tem as colunas que o store
|
|
62
|
+
* escreve. Existe por causa da adoção brownfield: quando o host aponta o
|
|
63
|
+
* AuthKit para a tabela `users` que ele JÁ tinha, as colunas dos mixins
|
|
64
|
+
* (`password_reset_token`, `email_verification_token`, …) não existem, o app
|
|
65
|
+
* sobe normalmente, e a falha só aparece quando alguém clica em "esqueci minha
|
|
66
|
+
* senha" — em produção. Este check antecipa isso para o boot.
|
|
67
|
+
*
|
|
68
|
+
* Silencioso quando não há o que inspecionar: um store custom (não-Lucid) não
|
|
69
|
+
* expõe `__model`, e nesse caso o contrato é responsabilidade de quem o
|
|
70
|
+
* implementou.
|
|
71
|
+
*/
|
|
72
|
+
export declare function checkAccountStoreColumns(input: DoctorInput): Finding[];
|
|
60
73
|
/** session provider configurado + warn se cookie store com tokenSets grandes. */
|
|
61
74
|
export declare function checkSession(input: DoctorInput): Finding[];
|
|
62
75
|
/** Hint de exceções de CSRF do shield para o mountPath. */
|
|
@@ -134,6 +134,84 @@ export function checkAccountStore(input) {
|
|
|
134
134
|
});
|
|
135
135
|
return findings;
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Propriedades do model que o account store Lucid escreve/lê, agrupadas pelo
|
|
139
|
+
* FLUXO que deixa de funcionar quando a coluna não existe. Agrupar por fluxo (e
|
|
140
|
+
* não listar colunas soltas) é o que torna o achado acionável: "falta
|
|
141
|
+
* `password_reset_token`" não diz nada a quem não conhece o interior da lib;
|
|
142
|
+
* "o reset de senha vai quebrar" diz.
|
|
143
|
+
*/
|
|
144
|
+
const REQUIRED_COLUMN_GROUPS = [
|
|
145
|
+
{ flow: 'identity (every flow)', properties: ['email'] },
|
|
146
|
+
{ flow: 'password login', properties: ['password'] },
|
|
147
|
+
{ flow: 'role claims / admin', properties: ['globalRoles'] },
|
|
148
|
+
{
|
|
149
|
+
flow: 'password reset',
|
|
150
|
+
properties: ['passwordResetToken', 'passwordResetExpiresAt'],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
flow: 'email verification',
|
|
154
|
+
properties: ['emailVerifiedAt', 'emailVerificationToken'],
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
/**
|
|
158
|
+
* Propriedades OPCIONAIS: a capability correspondente é detectada pela presença
|
|
159
|
+
* da coluna, então ausência é configuração válida (feature desligada) e nunca
|
|
160
|
+
* um erro — só vale reportar para que a ausência seja uma escolha, e não uma
|
|
161
|
+
* surpresa.
|
|
162
|
+
*/
|
|
163
|
+
const OPTIONAL_COLUMN_GROUPS = [
|
|
164
|
+
{ capability: 'profile (name/avatar)', properties: ['fullName', 'avatarUrl'] },
|
|
165
|
+
{ capability: 'disable/enable account', properties: ['disabledAt'] },
|
|
166
|
+
{ capability: 'password expiration', properties: ['passwordChangedAt'] },
|
|
167
|
+
];
|
|
168
|
+
/**
|
|
169
|
+
* Valida que o model por trás do account store tem as colunas que o store
|
|
170
|
+
* escreve. Existe por causa da adoção brownfield: quando o host aponta o
|
|
171
|
+
* AuthKit para a tabela `users` que ele JÁ tinha, as colunas dos mixins
|
|
172
|
+
* (`password_reset_token`, `email_verification_token`, …) não existem, o app
|
|
173
|
+
* sobe normalmente, e a falha só aparece quando alguém clica em "esqueci minha
|
|
174
|
+
* senha" — em produção. Este check antecipa isso para o boot.
|
|
175
|
+
*
|
|
176
|
+
* Silencioso quando não há o que inspecionar: um store custom (não-Lucid) não
|
|
177
|
+
* expõe `__model`, e nesse caso o contrato é responsabilidade de quem o
|
|
178
|
+
* implementou.
|
|
179
|
+
*/
|
|
180
|
+
export function checkAccountStoreColumns(input) {
|
|
181
|
+
const store = input.authkitConfig?.accountStore;
|
|
182
|
+
const model = store?.__model;
|
|
183
|
+
const definitions = model?.$columnsDefinitions;
|
|
184
|
+
if (!definitions || typeof definitions.get !== 'function')
|
|
185
|
+
return [];
|
|
186
|
+
/** Nome da coluna real (o que o DBA procura), com fallback à propriedade. */
|
|
187
|
+
const columnOf = (property) => definitions.get(property)?.columnName ?? property;
|
|
188
|
+
const missing = (properties) => properties.filter((p) => !definitions.has(p));
|
|
189
|
+
const findings = [];
|
|
190
|
+
for (const { flow, properties } of REQUIRED_COLUMN_GROUPS) {
|
|
191
|
+
const absent = missing(properties);
|
|
192
|
+
if (absent.length === 0)
|
|
193
|
+
continue;
|
|
194
|
+
const named = absent.map((p) => `\`${p}\` (column \`${columnOf(p)}\`)`).join(', ');
|
|
195
|
+
findings.push({
|
|
196
|
+
level: 'error',
|
|
197
|
+
message: `accountStore model (${model.name ?? 'model'}) is missing ${named} — ${flow} will fail at runtime. Add the column(s) with a migration, or map an existing one with @column({ columnName: '…' }).`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const off = OPTIONAL_COLUMN_GROUPS.filter((g) => missing(g.properties).length > 0);
|
|
201
|
+
if (off.length > 0) {
|
|
202
|
+
findings.push({
|
|
203
|
+
level: 'ok',
|
|
204
|
+
message: `Capabilities off (column absent): ${off.map((g) => g.capability).join(', ')}.`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (findings.every((f) => f.level === 'ok')) {
|
|
208
|
+
findings.unshift({
|
|
209
|
+
level: 'ok',
|
|
210
|
+
message: `accountStore model (${model.name ?? 'model'}) has every required column.`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return findings;
|
|
214
|
+
}
|
|
137
215
|
/** session provider configurado + warn se cookie store com tokenSets grandes. */
|
|
138
216
|
export function checkSession(input) {
|
|
139
217
|
if (!input.peers.session) {
|
|
@@ -845,6 +923,7 @@ export function runAllChecks(input) {
|
|
|
845
923
|
if (volatility)
|
|
846
924
|
findings.push(volatility);
|
|
847
925
|
findings.push(...checkAccountStore(input));
|
|
926
|
+
findings.push(...checkAccountStoreColumns(input));
|
|
848
927
|
findings.push(...checkSession(input));
|
|
849
928
|
findings.push(checkShield(input));
|
|
850
929
|
findings.push(checkAlly(input));
|
|
@@ -56,9 +56,13 @@ export declare function signWebhookBody(body: string, secret: string): string;
|
|
|
56
56
|
* e-mail). Nenhum data provider do dashboard lê `metadata`, então dropá-lo é
|
|
57
57
|
* seguro;
|
|
58
58
|
* - MANTÉM `type` (a família do evento — o que os providers agregam) e os ids
|
|
59
|
-
* internos opacos `accountId`/`actorId`/`clientId` (correlação de
|
|
60
|
-
* no dashboard; NÃO são PII direta e, sem `email`/`ip
|
|
61
|
-
* da conta já deletada, não são reidentificáveis).
|
|
59
|
+
* internos opacos `accountId`/`actorId`/`clientId`/`orgId` (correlação de
|
|
60
|
+
* subject/actor/tenant no dashboard; NÃO são PII direta e, sem `email`/`ip`/
|
|
61
|
+
* `metadata` e com a linha da conta já deletada, não são reidentificáveis).
|
|
62
|
+
* O `orgId` está aqui porque é o que torna o barramento UTILIZÁVEL para
|
|
63
|
+
* provisioning multi-tenant (o `@adonis-agora/authz` escuta
|
|
64
|
+
* `agora:authkit:organization.*` e precisa saber qual tenant escopar) —
|
|
65
|
+
* sem ele o consumidor recebia o tipo do evento e mais nada.
|
|
62
66
|
*
|
|
63
67
|
* Assim o Telescope nunca armazena PII bruta e a deleção de conta não precisa de uma
|
|
64
68
|
* etapa de purge cross-lib. Os ramos `onEvent`/`webhook` (integrações que o host
|
|
@@ -20,6 +20,7 @@ export function buildWebhookBody(event) {
|
|
|
20
20
|
accountId: event.accountId ?? null,
|
|
21
21
|
email: event.email ?? null,
|
|
22
22
|
clientId: event.clientId ?? null,
|
|
23
|
+
orgId: event.orgId ?? null,
|
|
23
24
|
ip: event.ip ?? null,
|
|
24
25
|
metadata: event.metadata ?? {},
|
|
25
26
|
ts: new Date().toISOString(),
|
|
@@ -50,9 +51,13 @@ export function signWebhookBody(body, secret) {
|
|
|
50
51
|
* e-mail). Nenhum data provider do dashboard lê `metadata`, então dropá-lo é
|
|
51
52
|
* seguro;
|
|
52
53
|
* - MANTÉM `type` (a família do evento — o que os providers agregam) e os ids
|
|
53
|
-
* internos opacos `accountId`/`actorId`/`clientId` (correlação de
|
|
54
|
-
* no dashboard; NÃO são PII direta e, sem `email`/`ip
|
|
55
|
-
* da conta já deletada, não são reidentificáveis).
|
|
54
|
+
* internos opacos `accountId`/`actorId`/`clientId`/`orgId` (correlação de
|
|
55
|
+
* subject/actor/tenant no dashboard; NÃO são PII direta e, sem `email`/`ip`/
|
|
56
|
+
* `metadata` e com a linha da conta já deletada, não são reidentificáveis).
|
|
57
|
+
* O `orgId` está aqui porque é o que torna o barramento UTILIZÁVEL para
|
|
58
|
+
* provisioning multi-tenant (o `@adonis-agora/authz` escuta
|
|
59
|
+
* `agora:authkit:organization.*` e precisa saber qual tenant escopar) —
|
|
60
|
+
* sem ele o consumidor recebia o tipo do evento e mais nada.
|
|
56
61
|
*
|
|
57
62
|
* Assim o Telescope nunca armazena PII bruta e a deleção de conta não precisa de uma
|
|
58
63
|
* etapa de purge cross-lib. Os ramos `onEvent`/`webhook` (integrações que o host
|
|
@@ -65,6 +70,7 @@ export function redactAuditEventForDiagnostics(event) {
|
|
|
65
70
|
accountId: event.accountId ?? null,
|
|
66
71
|
actorId: event.actorId ?? null,
|
|
67
72
|
clientId: event.clientId ?? null,
|
|
73
|
+
orgId: event.orgId ?? null,
|
|
68
74
|
};
|
|
69
75
|
}
|
|
70
76
|
/**
|
|
@@ -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) {
|
|
@@ -110,6 +110,7 @@ export class AdminOrgsService {
|
|
|
110
110
|
accountId: input.ownerAccountId,
|
|
111
111
|
actorId: actor.actorId,
|
|
112
112
|
ip: actor.ip,
|
|
113
|
+
orgId: org.id,
|
|
113
114
|
metadata: { slug: org.slug, ...(actor.source ? { actor: actor.source } : {}) },
|
|
114
115
|
});
|
|
115
116
|
return org;
|
|
@@ -137,6 +138,7 @@ export class AdminOrgsService {
|
|
|
137
138
|
type: 'organization.updated',
|
|
138
139
|
actorId: actor.actorId,
|
|
139
140
|
ip: actor.ip,
|
|
141
|
+
orgId,
|
|
140
142
|
metadata: { orgId, ...(actor.source ? { actor: actor.source } : {}) },
|
|
141
143
|
});
|
|
142
144
|
return updated;
|
|
@@ -158,6 +160,7 @@ export class AdminOrgsService {
|
|
|
158
160
|
type: 'organization.deleted',
|
|
159
161
|
actorId: actor.actorId,
|
|
160
162
|
ip: actor.ip,
|
|
163
|
+
orgId,
|
|
161
164
|
metadata: { orgId, slug: existing.slug, ...(actor.source ? { actor: actor.source } : {}) },
|
|
162
165
|
});
|
|
163
166
|
return { ok: true };
|
|
@@ -182,6 +185,7 @@ export class AdminOrgsService {
|
|
|
182
185
|
type: 'organization.member_added',
|
|
183
186
|
actorId: actor.actorId,
|
|
184
187
|
ip: actor.ip,
|
|
188
|
+
orgId,
|
|
185
189
|
metadata: {
|
|
186
190
|
orgId,
|
|
187
191
|
accountId: input.accountId,
|
|
@@ -209,6 +213,7 @@ export class AdminOrgsService {
|
|
|
209
213
|
type: 'organization.member_removed',
|
|
210
214
|
actorId: actor.actorId,
|
|
211
215
|
ip: actor.ip,
|
|
216
|
+
orgId,
|
|
212
217
|
metadata: {
|
|
213
218
|
orgId,
|
|
214
219
|
targetAccountId: accountId,
|
|
@@ -239,6 +244,7 @@ export class AdminOrgsService {
|
|
|
239
244
|
type: 'organization.member_role_changed',
|
|
240
245
|
actorId: actor.actorId,
|
|
241
246
|
ip: actor.ip,
|
|
247
|
+
orgId,
|
|
242
248
|
metadata: {
|
|
243
249
|
orgId,
|
|
244
250
|
targetAccountId: accountId,
|
|
@@ -301,6 +307,7 @@ export class AdminOrgsService {
|
|
|
301
307
|
type: 'organization.invitation_sent',
|
|
302
308
|
actorId: actor.actorId,
|
|
303
309
|
ip: actor.ip,
|
|
310
|
+
orgId,
|
|
304
311
|
metadata: {
|
|
305
312
|
orgId,
|
|
306
313
|
email: input.email,
|
|
@@ -327,6 +334,7 @@ export class AdminOrgsService {
|
|
|
327
334
|
type: 'organization.invitation_revoked',
|
|
328
335
|
actorId: actor.actorId,
|
|
329
336
|
ip: actor.ip,
|
|
337
|
+
orgId,
|
|
330
338
|
metadata: {
|
|
331
339
|
orgId,
|
|
332
340
|
invitationId,
|