@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.
- package/build/host/views/signup.edge +13 -5
- package/build/src/define_config.d.ts +9 -0
- package/build/src/define_config.js +1 -0
- package/build/src/host/controllers/registration_controller.d.ts +1 -0
- package/build/src/host/controllers/registration_controller.js +59 -2
- package/build/src/host/i18n.d.ts +2 -0
- package/build/src/host/i18n.js +2 -0
- 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)
|
|
@@ -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
|
/**
|
|
@@ -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');
|
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",
|
|
@@ -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.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.
|
|
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')\"",
|