@adonis-agora/authkit-server 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/host/views/signup.edge +13 -5
- package/build/index.d.ts +4 -4
- package/build/index.js +2 -2
- package/build/src/define_config.d.ts +49 -0
- package/build/src/define_config.js +17 -0
- package/build/src/host/config_locks.js +4 -0
- package/build/src/host/controllers/interaction_controller.js +1 -0
- package/build/src/host/controllers/registration_controller.d.ts +1 -0
- package/build/src/host/controllers/registration_controller.js +61 -2
- package/build/src/host/i18n.d.ts +2 -0
- package/build/src/host/i18n.js +2 -0
- package/build/src/host/runtime_toggles.d.ts +20 -0
- package/build/src/host/runtime_toggles.js +57 -33
- package/build/src/host/validators.d.ts +14 -0
- package/build/src/host/validators.js +5 -0
- package/package.json +2 -2
|
@@ -36,11 +36,19 @@
|
|
|
36
36
|
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
|
|
37
37
|
</div>
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
class="
|
|
43
|
-
|
|
39
|
+
{{-- Cadastro passwordless: sem campo de senha. O login vem por magic link. --}}
|
|
40
|
+
@if(!passwordlessSignup)
|
|
41
|
+
<div class="mt-4">
|
|
42
|
+
<label for="password" class="mb-1 block text-sm font-medium text-gray-700">{{ t('signup.password_label') }}</label>
|
|
43
|
+
<input id="password" name="password" type="password" required minlength="8"
|
|
44
|
+
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
|
|
45
|
+
</div>
|
|
46
|
+
@end
|
|
47
|
+
|
|
48
|
+
{{-- Passwordless: confirmação de link enviado (anti-enumeração). --}}
|
|
49
|
+
@if(magicLinkSent)
|
|
50
|
+
<p class="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">{{ t('signup.magic_link_sent') }}</p>
|
|
51
|
+
@end
|
|
44
52
|
|
|
45
53
|
{{-- Bot protection: container do widget (config-trusted; HTML do host renderizado raw). --}}
|
|
46
54
|
@if(botProtection && botProtection.html)
|
package/build/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@ export { registerOidcRoutes } from './src/register_routes.js';
|
|
|
13
13
|
export type { AuthServerConfigInput, ResolvedServerConfig, DynamicRegistrationConfigInput, ResolvedDynamicRegistrationConfig, AdminConfigInput, ResolvedAdminConfig, AdminApiConfigInput, ResolvedAdminApiConfig, } from './src/define_config.js';
|
|
14
14
|
export { resolveAdmin, resolveAdminApi, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
|
|
15
15
|
export type { WebauthnConfigInput, ResolvedWebauthnConfig } from './src/define_config.js';
|
|
16
|
-
export { resolvePasswordless, resolveLogin } from './src/define_config.js';
|
|
17
|
-
export type { PasswordlessConfigInput, ResolvedPasswordlessConfig, LoginConfigInput, ResolvedLoginConfig, } from './src/define_config.js';
|
|
16
|
+
export { resolvePasswordless, resolveLogin, resolveAuthMethodsConfig } from './src/define_config.js';
|
|
17
|
+
export type { PasswordlessConfigInput, ResolvedPasswordlessConfig, AuthMethodsConfigInput, ResolvedAuthMethodsConfig, LoginConfigInput, ResolvedLoginConfig, } from './src/define_config.js';
|
|
18
18
|
export { resolveTrustedDevices, isTrustedDeviceValid, buildTrustedDevicePayload, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
|
|
19
19
|
export type { TrustedDevicesConfigInput, ResolvedTrustedDevicesConfig, TrustedDevicePayload, } from './src/host/trusted_device.js';
|
|
20
20
|
export { resolveBotProtection, botProtectionApplies, extractBotToken, verifyBotProtection, guardBotProtection, DEFAULT_BOT_TOKEN_FIELDS, } from './src/host/bot_protection.js';
|
|
@@ -105,8 +105,8 @@ export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
|
105
105
|
export type { KeysStatus as ServerKeysStatus } from './src/host/key_rotation_actions.js';
|
|
106
106
|
export { resolveEffectiveBotProtection, } from './src/host/bot_protection.js';
|
|
107
107
|
export type { BotProtectionSetting } from './src/host/bot_protection.js';
|
|
108
|
-
export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, } from './src/host/runtime_toggles.js';
|
|
109
|
-
export type { SettingKey, RegistrationSetting, RequireVerifiedEmailSetting, MaintenanceModeSetting, ResolvedMaintenanceMode, } from './src/host/runtime_toggles.js';
|
|
108
|
+
export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, resolveEffectiveAuthMethods, configLockedAuthMethods, } from './src/host/runtime_toggles.js';
|
|
109
|
+
export type { SettingKey, RegistrationSetting, RequireVerifiedEmailSetting, MaintenanceModeSetting, ResolvedMaintenanceMode, AuthMethodsSetting, ResolvedAuthMethods, AuthMethodsCapabilities, AuthMethodsConfigOverride, } from './src/host/runtime_toggles.js';
|
|
110
110
|
export { resolveRegistration } from './src/define_config.js';
|
|
111
111
|
export type { RegistrationConfigInput, ResolvedRegistrationConfig, } from './src/define_config.js';
|
|
112
112
|
export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
|
package/build/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { withMfa } from './src/mixins/with_mfa.js';
|
|
|
9
9
|
export { OidcService } from './src/provider/oidc_service.js';
|
|
10
10
|
export { registerOidcRoutes } from './src/register_routes.js';
|
|
11
11
|
export { resolveAdmin, resolveAdminApi, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
|
|
12
|
-
export { resolvePasswordless, resolveLogin } from './src/define_config.js';
|
|
12
|
+
export { resolvePasswordless, resolveLogin, resolveAuthMethodsConfig } from './src/define_config.js';
|
|
13
13
|
export { resolveTrustedDevices, isTrustedDeviceValid, buildTrustedDevicePayload, TRUSTED_DEVICE_COOKIE, } from './src/host/trusted_device.js';
|
|
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';
|
|
@@ -68,7 +68,7 @@ export { RuntimeSettings, supportsSettings } from './src/host/runtime_settings.j
|
|
|
68
68
|
export { buildKeysStatus, rotateNow } from './src/host/key_rotation_actions.js';
|
|
69
69
|
export { resolveEffectiveBotProtection, } from './src/host/bot_protection.js';
|
|
70
70
|
// Runtime toggles (registration, require_verified_email, maintenance_mode).
|
|
71
|
-
export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, } from './src/host/runtime_toggles.js';
|
|
71
|
+
export { SETTING_KEYS, resolveEffectiveRegistration, resolveEffectiveRequireVerifiedEmail, resolveEffectiveMaintenanceMode, resolveEffectiveAuthMethods, configLockedAuthMethods, } from './src/host/runtime_toggles.js';
|
|
72
72
|
export { resolveRegistration } from './src/define_config.js';
|
|
73
73
|
export { getAccountId, hasAccountSession, consoleLoginUrl, } from './src/host/console_session.js';
|
|
74
74
|
export { ACCOUNT_SESSION_KEY } from './src/host/middleware/account_auth.js';
|
|
@@ -322,12 +322,53 @@ export interface PasswordlessConfigInput {
|
|
|
322
322
|
magicLink?: boolean;
|
|
323
323
|
/** Liga o "entrar com passkey" antes da senha. Default: false. */
|
|
324
324
|
passkeyFirst?: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Liga o cadastro público passwordless: o signup pede só e-mail + nome (sem
|
|
327
|
+
* senha), cria a conta com uma senha aleatória inutilizável e envia um magic
|
|
328
|
+
* link que finaliza o login (o mesmo fluxo do login por magic link). Default:
|
|
329
|
+
* false. Exige que o accountStore implemente {@link MagicLinkCapability}; sem
|
|
330
|
+
* ela o cadastro passwordless fica indisponível e o fluxo de senha segue valendo.
|
|
331
|
+
*/
|
|
332
|
+
signup?: boolean;
|
|
325
333
|
}
|
|
326
334
|
export interface ResolvedPasswordlessConfig {
|
|
327
335
|
magicLink: boolean;
|
|
328
336
|
passkeyFirst: boolean;
|
|
337
|
+
signup: boolean;
|
|
329
338
|
}
|
|
330
339
|
export declare function resolvePasswordless(input?: PasswordlessConfigInput): ResolvedPasswordlessConfig;
|
|
340
|
+
/**
|
|
341
|
+
* Fixa métodos de login PELO ARQUIVO DE CONFIG, com PRIORIDADE sobre o runtime
|
|
342
|
+
* setting `auth_methods` (o inverso do default — normalmente o runtime manda).
|
|
343
|
+
*
|
|
344
|
+
* Cada campo declarado aqui é AUTORITATIVO: o console admin não sobrescreve em
|
|
345
|
+
* runtime (a UI mostra o toggle travado, "definido no config"). Campos omitidos
|
|
346
|
+
* continuam controlados pelo runtime → default por capability.
|
|
347
|
+
*
|
|
348
|
+
* Guards preservados no resolver:
|
|
349
|
+
* - Fixar um método em `true` só o liga se a CAPACIDADE existir (ex.: `magicLink: true`
|
|
350
|
+
* sem store capaz continua oculto). Fixar em `false` é sempre respeitado.
|
|
351
|
+
* - Fail-safe all-off: se pins + runtime zerarem TODOS os métodos, volta ao default
|
|
352
|
+
* (nunca trancar todo mundo pra fora).
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* // Login sem senha (magic-link + passkey), travado pelo config — sem comando por ambiente:
|
|
356
|
+
* authMethods: { password: false }
|
|
357
|
+
*/
|
|
358
|
+
export interface AuthMethodsConfigInput {
|
|
359
|
+
/** Fixa o login por senha. */
|
|
360
|
+
password?: boolean;
|
|
361
|
+
/** Fixa o login por magic link (respeitando a capacidade do store). */
|
|
362
|
+
magicLink?: boolean;
|
|
363
|
+
/** Fixa o "entrar com passkey" (respeitando a capacidade do store). */
|
|
364
|
+
passkey?: boolean;
|
|
365
|
+
/** Fixa o link "esqueci a senha" (só aparece com senha ligada, de qualquer forma). */
|
|
366
|
+
forgotPassword?: boolean;
|
|
367
|
+
}
|
|
368
|
+
/** authMethods resolvido: só os campos que o config FIXOU (os demais ficam a cargo do runtime). */
|
|
369
|
+
export type ResolvedAuthMethodsConfig = AuthMethodsConfigInput;
|
|
370
|
+
/** Normaliza o input: mantém só os campos booleanos declarados (os pins). */
|
|
371
|
+
export declare function resolveAuthMethodsConfig(input?: AuthMethodsConfigInput): ResolvedAuthMethodsConfig;
|
|
331
372
|
/**
|
|
332
373
|
* Política de login por senha/identidade. Hoje cobre `requireVerifiedEmail`:
|
|
333
374
|
* quando ligado, TODO fluxo de login que materializa uma sessão (login por senha,
|
|
@@ -595,6 +636,12 @@ export interface AuthServerConfigInput {
|
|
|
595
636
|
* desligados. Exigem as capacidades correspondentes no accountStore.
|
|
596
637
|
*/
|
|
597
638
|
passwordless?: PasswordlessConfigInput;
|
|
639
|
+
/**
|
|
640
|
+
* Fixa métodos de login pelo arquivo de config, com PRIORIDADE sobre o runtime
|
|
641
|
+
* setting `auth_methods`. Cada campo declarado trava o método (o console admin não
|
|
642
|
+
* sobrescreve). Default: `{}` (tudo controlado pelo runtime). Ver {@link AuthMethodsConfigInput}.
|
|
643
|
+
*/
|
|
644
|
+
authMethods?: AuthMethodsConfigInput;
|
|
598
645
|
/**
|
|
599
646
|
* Política de login (hoje: `requireVerifiedEmail`). Default: tudo desligado.
|
|
600
647
|
* `requireVerifiedEmail` é capability-probed (precisa de `isEmailVerified` no store).
|
|
@@ -749,6 +796,8 @@ export interface ResolvedServerConfig {
|
|
|
749
796
|
botProtection?: ResolvedBotProtectionConfig;
|
|
750
797
|
/** Passwordless resolvido (default tudo desligado). */
|
|
751
798
|
passwordless: ResolvedPasswordlessConfig;
|
|
799
|
+
/** Métodos de login fixados pelo config (prioridade sobre runtime; default `{}`). */
|
|
800
|
+
authMethods: ResolvedAuthMethodsConfig;
|
|
752
801
|
/** Política de login resolvida (requireVerifiedEmail; default desligado). */
|
|
753
802
|
login: ResolvedLoginConfig;
|
|
754
803
|
/** Configuração de cadastro público resolvida (enabled; default true). */
|
|
@@ -92,8 +92,24 @@ export function resolvePasswordless(input) {
|
|
|
92
92
|
return {
|
|
93
93
|
magicLink: input?.magicLink ?? false,
|
|
94
94
|
passkeyFirst: input?.passkeyFirst ?? false,
|
|
95
|
+
signup: input?.signup ?? false,
|
|
95
96
|
};
|
|
96
97
|
}
|
|
98
|
+
/** Normaliza o input: mantém só os campos booleanos declarados (os pins). */
|
|
99
|
+
export function resolveAuthMethodsConfig(input) {
|
|
100
|
+
if (!input)
|
|
101
|
+
return {};
|
|
102
|
+
const out = {};
|
|
103
|
+
if (typeof input.password === "boolean")
|
|
104
|
+
out.password = input.password;
|
|
105
|
+
if (typeof input.magicLink === "boolean")
|
|
106
|
+
out.magicLink = input.magicLink;
|
|
107
|
+
if (typeof input.passkey === "boolean")
|
|
108
|
+
out.passkey = input.passkey;
|
|
109
|
+
if (typeof input.forgotPassword === "boolean")
|
|
110
|
+
out.forgotPassword = input.forgotPassword;
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
97
113
|
export function resolveLogin(input) {
|
|
98
114
|
return {
|
|
99
115
|
requireVerifiedEmail: input?.requireVerifiedEmail ?? false,
|
|
@@ -334,6 +350,7 @@ export function defineConfig(config) {
|
|
|
334
350
|
trustedDevices: resolveTrustedDevices(config.trustedDevices),
|
|
335
351
|
botProtection: resolveBotProtection(config.botProtection),
|
|
336
352
|
passwordless: resolvePasswordless(config.passwordless),
|
|
353
|
+
authMethods: resolveAuthMethodsConfig(config.authMethods),
|
|
337
354
|
login: resolveLogin(config.login),
|
|
338
355
|
registration: resolveRegistration(config.registration),
|
|
339
356
|
accessTokens: resolveAccessTokens(config.issuer, config.accessTokens),
|
|
@@ -29,6 +29,10 @@ export function deriveLockedSettingKeys(config) {
|
|
|
29
29
|
locked.push(key);
|
|
30
30
|
};
|
|
31
31
|
add(config.registration !== undefined, SETTING_KEYS.REGISTRATION);
|
|
32
|
+
// authMethods no config trava a key inteira `auth_methods`: os métodos passam a ser
|
|
33
|
+
// controlados pelo arquivo (via cfg.authMethods → configOverrides no resolver) e a UI
|
|
34
|
+
// desabilita os toggles com "definido via defineConfig".
|
|
35
|
+
add(config.authMethods !== undefined, SETTING_KEYS.AUTH_METHODS);
|
|
32
36
|
add(config.login?.requireVerifiedEmail !== undefined, SETTING_KEYS.REQUIRE_VERIFIED_EMAIL);
|
|
33
37
|
add(config.lockout !== undefined, SETTING_KEYS.LOCKOUT);
|
|
34
38
|
add(config.rateLimit !== undefined, SETTING_KEYS.RATE_LIMIT);
|
|
@@ -87,6 +87,7 @@ export default class AuthInteractionController {
|
|
|
87
87
|
configuredSocialProviders,
|
|
88
88
|
magicLinkCapable: magicLinkCapableConfig,
|
|
89
89
|
passkeyCapable: false, // passkey-first depends on account — resolved in step 2
|
|
90
|
+
configOverrides: cfg.authMethods,
|
|
90
91
|
});
|
|
91
92
|
if (!email) {
|
|
92
93
|
// Step 1: identifier (email only).
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
3
|
export default class AuthRegistrationController {
|
|
4
|
+
#private;
|
|
4
5
|
/** GET /auth/interaction/:uid/signup — tela de cadastro (dentro do fluxo OIDC). */
|
|
5
6
|
showSignup(ctx: HttpContext): Promise<any>;
|
|
6
7
|
/** POST /auth/interaction/:uid/signup — cria o usuário e finaliza o login. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import '../augmentations.js';
|
|
2
2
|
import { brandFor } from '../branding.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { signupValidator, passwordlessSignupValidator, forgotPasswordValidator, resetPasswordValidator, } from '../validators.js';
|
|
5
|
+
import { sendPasswordResetEmail, sendEmailVerificationEmail, sendMagicLinkEmail, } from '../default_mailer.js';
|
|
5
6
|
import { translate } from '../i18n.js';
|
|
6
7
|
import { PasswordPolicyError } from '../../password/password_manager.js';
|
|
7
8
|
import { guardBotProtection, resolveEffectiveBotProtection } from '../bot_protection.js';
|
|
@@ -54,6 +55,7 @@ export default class AuthRegistrationController {
|
|
|
54
55
|
uid: details.uid,
|
|
55
56
|
csrfToken: ctx.request.csrfToken,
|
|
56
57
|
brand,
|
|
58
|
+
passwordlessSignup: cfg.passwordless?.signup ?? false,
|
|
57
59
|
botProtection: effectiveBot?.on.includes('signup') ? effectiveBot.widget : undefined,
|
|
58
60
|
});
|
|
59
61
|
}
|
|
@@ -102,6 +104,12 @@ export default class AuthRegistrationController {
|
|
|
102
104
|
botProtection: effectiveBotSignup?.widget,
|
|
103
105
|
});
|
|
104
106
|
}
|
|
107
|
+
// Cadastro passwordless (config): só e-mail + nome. Cria conta com senha random
|
|
108
|
+
// inutilizável e envia um magic link — mesmo fluxo do login por magic link.
|
|
109
|
+
if (cfg.passwordless?.signup &&
|
|
110
|
+
typeof cfg.accountStore.issueMagicLinkToken === 'function') {
|
|
111
|
+
return this.#passwordlessSignup(ctx, { cfg, brand, details });
|
|
112
|
+
}
|
|
105
113
|
const data = await ctx.request.validateUsing(signupValidator);
|
|
106
114
|
const accountStore = cfg.accountStore;
|
|
107
115
|
const existing = await accountStore.findByEmail(data.email);
|
|
@@ -183,6 +191,55 @@ export default class AuthRegistrationController {
|
|
|
183
191
|
ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar verificação de e-mail');
|
|
184
192
|
}
|
|
185
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Cadastro passwordless: valida e-mail + nome, cria a conta (senha random
|
|
196
|
+
* inutilizável) se ainda não existe, emite um magic link e o envia. Sempre
|
|
197
|
+
* responde "link enviado" (anti-enumeração), exista a conta ou não. Consumir o
|
|
198
|
+
* link finaliza o login pelo fluxo de magic link já existente (GET /magic).
|
|
199
|
+
*/
|
|
200
|
+
async #passwordlessSignup(ctx, deps) {
|
|
201
|
+
const { cfg, brand, details } = deps;
|
|
202
|
+
const render = cfg.render;
|
|
203
|
+
const accountStore = cfg.accountStore;
|
|
204
|
+
const uid = ctx.request.param('uid');
|
|
205
|
+
const data = await ctx.request.validateUsing(passwordlessSignupValidator);
|
|
206
|
+
// Cria a conta se ainda não existe. Senha random inutilizável: o login é 100%
|
|
207
|
+
// passwordless (mesmo precedente das contas criadas por identidade social).
|
|
208
|
+
const existing = await accountStore.findByEmail(data.email);
|
|
209
|
+
if (!existing) {
|
|
210
|
+
const created = await accountStore.create({
|
|
211
|
+
email: data.email,
|
|
212
|
+
fullName: data.fullName,
|
|
213
|
+
password: randomBytes(24).toString('hex'),
|
|
214
|
+
});
|
|
215
|
+
await cfg.audit?.record({
|
|
216
|
+
type: 'signup',
|
|
217
|
+
accountId: created.id,
|
|
218
|
+
email: data.email,
|
|
219
|
+
ip: ctx.request.ip?.() ?? null,
|
|
220
|
+
clientId: details.params.client_id ?? null,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// Emite + envia o magic link (mesma construção do login por magic link).
|
|
224
|
+
const issued = await accountStore.issueMagicLinkToken(data.email);
|
|
225
|
+
if (issued) {
|
|
226
|
+
const origin = `${ctx.request.protocol()}://${ctx.request.host()}`;
|
|
227
|
+
const magicUrl = `${origin}/auth/interaction/${uid}/magic?token=${encodeURIComponent(issued.token)}`;
|
|
228
|
+
if (cfg.mail?.onMagicLink) {
|
|
229
|
+
await cfg.mail.onMagicLink({ email: data.email, magicUrl, token: issued.token });
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
await sendMagicLinkEmail(ctx, { email: data.email, magicUrl });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Resposta uniforme: "enviamos um link" (não vaza existência da conta).
|
|
236
|
+
return render(ctx, 'signup', {
|
|
237
|
+
uid,
|
|
238
|
+
csrfToken: ctx.request.csrfToken,
|
|
239
|
+
brand,
|
|
240
|
+
magicLinkSent: true,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
186
243
|
/** GET /auth/forgot-password — tela standalone. */
|
|
187
244
|
async showForgot(ctx) {
|
|
188
245
|
const service = await ctx.containerResolver.make('authkit.server');
|
|
@@ -201,6 +258,7 @@ export default class AuthRegistrationController {
|
|
|
201
258
|
const authMethods = await resolveEffectiveAuthMethods(runtimeSettings, {
|
|
202
259
|
configuredSocialProviders: cfg.social?.providers ?? [],
|
|
203
260
|
magicLinkCapable: cfg.passwordless?.magicLink && typeof cfg.accountStore.issueMagicLinkToken === 'function',
|
|
261
|
+
configOverrides: cfg.authMethods,
|
|
204
262
|
});
|
|
205
263
|
if (!authMethods.forgotPassword) {
|
|
206
264
|
return ctx.response.notFound({ error: translate(cfg.messages, 'errors.not_found') });
|
|
@@ -229,6 +287,7 @@ export default class AuthRegistrationController {
|
|
|
229
287
|
const authMethods = await resolveEffectiveAuthMethods(runtimeSettings, {
|
|
230
288
|
configuredSocialProviders: cfg.social?.providers ?? [],
|
|
231
289
|
magicLinkCapable: cfg.passwordless?.magicLink && typeof cfg.accountStore.issueMagicLinkToken === 'function',
|
|
290
|
+
configOverrides: cfg.authMethods,
|
|
232
291
|
});
|
|
233
292
|
if (!authMethods.forgotPassword) {
|
|
234
293
|
return ctx.response.notFound({ error: translate(cfg.messages, 'errors.not_found') });
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
48
48
|
"account.login.idle_timeout": string;
|
|
49
49
|
"login.magic_link_button": string;
|
|
50
50
|
"login.magic_link_sent": string;
|
|
51
|
+
"signup.magic_link_sent": string;
|
|
51
52
|
"login.passkey_button": string;
|
|
52
53
|
"signup.page_title": string;
|
|
53
54
|
"signup.title": string;
|
|
@@ -734,6 +735,7 @@ export declare const PT_BR_MESSAGES: {
|
|
|
734
735
|
"account.login.idle_timeout": string;
|
|
735
736
|
"login.magic_link_button": string;
|
|
736
737
|
"login.magic_link_sent": string;
|
|
738
|
+
"signup.magic_link_sent": string;
|
|
737
739
|
"login.passkey_button": string;
|
|
738
740
|
"signup.page_title": string;
|
|
739
741
|
"signup.title": string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -39,6 +39,7 @@ export const DEFAULT_MESSAGES = {
|
|
|
39
39
|
// Passwordless (login).
|
|
40
40
|
"login.magic_link_button": "Email me a login link",
|
|
41
41
|
"login.magic_link_sent": "If the account exists, we sent you a login link.",
|
|
42
|
+
"signup.magic_link_sent": "Check your email — we sent you a link to finish creating your account.",
|
|
42
43
|
"login.passkey_button": "Sign in with a passkey",
|
|
43
44
|
// Tela de cadastro (signup).
|
|
44
45
|
"signup.page_title": "Create account",
|
|
@@ -803,6 +804,7 @@ export const PT_BR_MESSAGES = {
|
|
|
803
804
|
// Passwordless (login).
|
|
804
805
|
"login.magic_link_button": "Me envie um link de login",
|
|
805
806
|
"login.magic_link_sent": "Se a conta existir, enviamos um link de login.",
|
|
807
|
+
"signup.magic_link_sent": "Enviamos um link para o seu e-mail. Abra-o para concluir o cadastro.",
|
|
806
808
|
"login.passkey_button": "Entrar com passkey",
|
|
807
809
|
// Tela de cadastro (signup).
|
|
808
810
|
"signup.page_title": "Criar conta",
|
|
@@ -202,7 +202,27 @@ export interface AuthMethodsCapabilities {
|
|
|
202
202
|
magicLinkCapable?: boolean;
|
|
203
203
|
/** Passkey-first disponível (config.passwordless.passkeyFirst && store tem passkeys). */
|
|
204
204
|
passkeyCapable?: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Pins vindos do ARQUIVO DE CONFIG (`cfg.authMethods`): têm PRIORIDADE sobre o
|
|
207
|
+
* runtime setting `auth_methods`. Cada campo presente sobrescreve o valor resolvido
|
|
208
|
+
* do setting. Ligar respeita as capacidades (magicLink/passkey só ligam se capable);
|
|
209
|
+
* desligar é sempre respeitado. Campos ausentes seguem controlados pelo runtime.
|
|
210
|
+
*/
|
|
211
|
+
configOverrides?: AuthMethodsConfigOverride;
|
|
205
212
|
}
|
|
213
|
+
/** Métodos que o config pode FIXAR (prioridade sobre runtime). Só os campos declarados travam. */
|
|
214
|
+
export interface AuthMethodsConfigOverride {
|
|
215
|
+
password?: boolean;
|
|
216
|
+
magicLink?: boolean;
|
|
217
|
+
passkey?: boolean;
|
|
218
|
+
forgotPassword?: boolean;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Quais métodos estão TRAVADOS pelo config (para a UI admin mostrar o toggle
|
|
222
|
+
* desabilitado com "definido no arquivo de config"). É só a lista de campos
|
|
223
|
+
* booleanos presentes em `configOverrides`.
|
|
224
|
+
*/
|
|
225
|
+
export declare function configLockedAuthMethods(configOverrides?: AuthMethodsConfigOverride): Array<keyof AuthMethodsConfigOverride>;
|
|
206
226
|
/**
|
|
207
227
|
* Resolve os métodos de autenticação efetivos combinando config estático + setting
|
|
208
228
|
* persistida em runtime + capabilities.
|
|
@@ -127,6 +127,22 @@ export async function resolveEffectiveMaintenanceMode(settings) {
|
|
|
127
127
|
message: typeof setting.message === 'string' ? setting.message : undefined,
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Quais métodos estão TRAVADOS pelo config (para a UI admin mostrar o toggle
|
|
132
|
+
* desabilitado com "definido no arquivo de config"). É só a lista de campos
|
|
133
|
+
* booleanos presentes em `configOverrides`.
|
|
134
|
+
*/
|
|
135
|
+
export function configLockedAuthMethods(configOverrides) {
|
|
136
|
+
if (!configOverrides)
|
|
137
|
+
return [];
|
|
138
|
+
const keys = [
|
|
139
|
+
'password',
|
|
140
|
+
'magicLink',
|
|
141
|
+
'passkey',
|
|
142
|
+
'forgotPassword',
|
|
143
|
+
];
|
|
144
|
+
return keys.filter((k) => typeof configOverrides[k] === 'boolean');
|
|
145
|
+
}
|
|
130
146
|
/**
|
|
131
147
|
* Resolve os métodos de autenticação efetivos combinando config estático + setting
|
|
132
148
|
* persistida em runtime + capabilities.
|
|
@@ -143,7 +159,7 @@ export async function resolveEffectiveMaintenanceMode(settings) {
|
|
|
143
159
|
* 6. Qualquer erro DB → defaults derivados do config (fail-safe total).
|
|
144
160
|
*/
|
|
145
161
|
export async function resolveEffectiveAuthMethods(settings, capabilities = {}) {
|
|
146
|
-
const { configuredSocialProviders = [], magicLinkCapable = false, passkeyCapable = false, } = capabilities;
|
|
162
|
+
const { configuredSocialProviders = [], magicLinkCapable = false, passkeyCapable = false, configOverrides, } = capabilities;
|
|
147
163
|
// Defaults derivados do config/capabilities.
|
|
148
164
|
const configDefaults = {
|
|
149
165
|
password: true,
|
|
@@ -153,47 +169,55 @@ export async function resolveEffectiveAuthMethods(settings, capabilities = {}) {
|
|
|
153
169
|
forgotPassword: true,
|
|
154
170
|
passkeyAutofill: passkeyCapable, // on por default quando passkey disponível
|
|
155
171
|
};
|
|
172
|
+
// 1) Runtime setting sobre os defaults (setting ausente/inválida → só defaults).
|
|
156
173
|
const raw = await settings.getSetting(SETTING_KEYS.AUTH_METHODS);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const passwordEnabled = typeof s.password === 'boolean' ? s.password : configDefaults.password;
|
|
164
|
-
const magicLinkEnabled = typeof s.magicLink === 'boolean' ? s.magicLink : configDefaults.magicLink;
|
|
165
|
-
const passkeyEnabled = typeof s.passkey === 'boolean' ? s.passkey : configDefaults.passkey;
|
|
174
|
+
const s = raw !== null && raw !== undefined && typeof raw === 'object' && !Array.isArray(raw)
|
|
175
|
+
? raw
|
|
176
|
+
: {};
|
|
177
|
+
let password = typeof s.password === 'boolean' ? s.password : configDefaults.password;
|
|
178
|
+
let magicLink = typeof s.magicLink === 'boolean' ? s.magicLink : configDefaults.magicLink;
|
|
179
|
+
let passkey = typeof s.passkey === 'boolean' ? s.passkey : configDefaults.passkey;
|
|
166
180
|
// Social: interseção com os providers do config (setting não pode ligar o que não existe).
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
const social = Array.isArray(s.social)
|
|
182
|
+
? s.social.filter((p) => configuredSocialProviders.includes(p))
|
|
183
|
+
: configDefaults.social;
|
|
184
|
+
// 2) Pins do config: PRIORIDADE sobre o setting. Ligar respeita a capacidade;
|
|
185
|
+
// desligar sempre vale. `password` não tem guard de capacidade.
|
|
186
|
+
if (configOverrides) {
|
|
187
|
+
if (typeof configOverrides.password === 'boolean')
|
|
188
|
+
password = configOverrides.password;
|
|
189
|
+
if (typeof configOverrides.magicLink === 'boolean')
|
|
190
|
+
magicLink = configOverrides.magicLink && magicLinkCapable;
|
|
191
|
+
if (typeof configOverrides.passkey === 'boolean')
|
|
192
|
+
passkey = configOverrides.passkey && passkeyCapable;
|
|
193
|
+
}
|
|
194
|
+
// forgotPassword: derivado — sem senha NUNCA existe esqueci-senha. O pin/setting só
|
|
195
|
+
// pode DESLIGAR (nunca ligar sem senha). Precedência: pin do config > setting > default(on).
|
|
196
|
+
const forgotPasswordRequested = typeof configOverrides?.forgotPassword === 'boolean'
|
|
197
|
+
? configOverrides.forgotPassword
|
|
198
|
+
: typeof s.forgotPassword === 'boolean'
|
|
199
|
+
? s.forgotPassword
|
|
200
|
+
: true;
|
|
201
|
+
const forgotPassword = password && forgotPasswordRequested;
|
|
176
202
|
// passkeyAutofill: só faz sentido quando passkey está ligado.
|
|
177
|
-
const
|
|
203
|
+
const passkeyAutofill = passkey &&
|
|
178
204
|
(typeof s.passkeyAutofill === 'boolean' ? s.passkeyAutofill : configDefaults.passkeyAutofill);
|
|
179
205
|
const resolved = {
|
|
180
|
-
password
|
|
181
|
-
magicLink
|
|
182
|
-
passkey
|
|
183
|
-
social
|
|
184
|
-
forgotPassword
|
|
185
|
-
passkeyAutofill
|
|
206
|
+
password,
|
|
207
|
+
magicLink,
|
|
208
|
+
passkey,
|
|
209
|
+
social,
|
|
210
|
+
forgotPassword,
|
|
211
|
+
passkeyAutofill,
|
|
186
212
|
};
|
|
187
|
-
// FAIL-SAFE all-off: se
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
!resolved.passkey &&
|
|
191
|
-
resolved.social.length === 0;
|
|
213
|
+
// FAIL-SAFE all-off: se pins + setting zerarem TODOS os métodos, volta ao config derivado
|
|
214
|
+
// (nunca deixar a tela sem método / trancar todo mundo pra fora).
|
|
215
|
+
const allOff = !resolved.password && !resolved.magicLink && !resolved.passkey && resolved.social.length === 0;
|
|
192
216
|
if (allOff) {
|
|
193
217
|
// eslint-disable-next-line no-console
|
|
194
|
-
console.warn('[authkit] auth_methods setting deixou todos os métodos desligados — ' +
|
|
218
|
+
console.warn('[authkit] auth_methods (config pins + setting) deixou todos os métodos desligados — ' +
|
|
195
219
|
'revertendo para os defaults do config (fail-safe). ' +
|
|
196
|
-
'Verifique a setting auth_methods no console admin.');
|
|
220
|
+
'Verifique cfg.authMethods e a setting auth_methods no console admin.');
|
|
197
221
|
return configDefaults;
|
|
198
222
|
}
|
|
199
223
|
return resolved;
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/** Cadastro passwordless: só e-mail + nome (sem senha). O login vem por magic link. */
|
|
2
|
+
export declare const passwordlessSignupValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
|
|
3
|
+
email: import("@vinejs/vine").VineString;
|
|
4
|
+
fullName: import("@vinejs/vine").VineString;
|
|
5
|
+
}, {
|
|
6
|
+
email: string;
|
|
7
|
+
fullName: string;
|
|
8
|
+
}, {
|
|
9
|
+
email: string;
|
|
10
|
+
fullName: string;
|
|
11
|
+
}, {
|
|
12
|
+
email: string;
|
|
13
|
+
fullName: string;
|
|
14
|
+
}>, Record<string, any> | undefined>;
|
|
1
15
|
export declare const signupValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
|
|
2
16
|
email: import("@vinejs/vine").VineString;
|
|
3
17
|
fullName: import("@vinejs/vine").VineString;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import vine from '@vinejs/vine';
|
|
2
|
+
/** Cadastro passwordless: só e-mail + nome (sem senha). O login vem por magic link. */
|
|
3
|
+
export const passwordlessSignupValidator = vine.compile(vine.object({
|
|
4
|
+
email: vine.string().trim().email().normalizeEmail(),
|
|
5
|
+
fullName: vine.string().trim().minLength(2).maxLength(255),
|
|
6
|
+
}));
|
|
2
7
|
export const signupValidator = vine.compile(vine.object({
|
|
3
8
|
email: vine.string().trim().email().normalizeEmail(),
|
|
4
9
|
fullName: vine.string().trim().minLength(2).maxLength(255),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
"react-error-boundary": "6.1.2",
|
|
141
141
|
"nuqs": "2.8.9",
|
|
142
142
|
"recharts": "3.8.1",
|
|
143
|
-
"@adonis-agora/authkit-react": "0.
|
|
143
|
+
"@adonis-agora/authkit-react": "0.16.0"
|
|
144
144
|
},
|
|
145
145
|
"scripts": {
|
|
146
146
|
"build": "node scripts/build_host_css.mjs && node scripts/build_ui.mjs && tsc && node -e \"require('node:fs').cpSync('stubs','build/stubs',{recursive:true,filter:(s)=>!s.endsWith('.ts')})\" && node -e \"const fs=require('node:fs');if(fs.existsSync('assets'))fs.cpSync('assets','build/assets',{recursive:true})\" && node -e \"require('node:fs').cpSync('src/host/views','build/host/views',{recursive:true})\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/host/ui',{recursive:true});fs.readdirSync('src/host/ui').filter(f=>f.endsWith('.html')).forEach(f=>fs.copyFileSync('src/host/ui/'+f,'build/host/ui/'+f))\" && node -e \"require('node:fs').copyFileSync('commands/commands.json','build/commands/commands.json')\" && node -e \"const fs=require('node:fs');fs.mkdirSync('build/password',{recursive:true});fs.copyFileSync('src/password/common_passwords.txt','build/password/common_passwords.txt')\"",
|