@adonis-agora/authkit-server 0.35.0 → 0.36.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.
@@ -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
- <div class="mt-4">
40
- <label for="password" class="mb-1 block text-sm font-medium text-gray-700">{{ t('signup.password_label') }}</label>
41
- <input id="password" name="password" type="password" required minlength="8"
42
- 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" />
43
- </div>
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)
@@ -322,10 +322,19 @@ 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;
331
340
  /**
@@ -92,6 +92,7 @@ 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
  }
97
98
  export function resolveLogin(input) {
@@ -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 { signupValidator, forgotPasswordValidator, resetPasswordValidator } from '../validators.js';
4
- import { sendPasswordResetEmail, sendEmailVerificationEmail } from '../default_mailer.js';
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');
@@ -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;
@@ -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",
@@ -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.35.0",
3
+ "version": "0.36.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.13.1"
143
+ "@adonis-agora/authkit-react": "0.14.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')\"",