@dudousxd/adonis-authkit-server 0.5.0 → 0.6.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/README.md +23 -2
- package/build/host/views/account/apps.edge +58 -0
- package/build/host/views/account/security.edge +53 -0
- package/build/host/views/account/tokens.edge +1 -0
- package/build/host/views/admin/users.edge +62 -2
- package/build/host/views/login.edge +55 -0
- package/build/host/views/mfa-challenge.edge +12 -0
- package/build/index.d.ts +8 -2
- package/build/index.js +4 -1
- package/build/src/accounts/account_store.d.ts +80 -2
- package/build/src/accounts/account_store.js +12 -0
- package/build/src/accounts/lucid_account_store.js +8 -0
- package/build/src/accounts/lucid_store/core.d.ts +2 -2
- package/build/src/accounts/lucid_store/core.js +33 -0
- package/build/src/accounts/lucid_store/mfa.js +4 -1
- package/build/src/accounts/lucid_store/status_profile.d.ts +21 -0
- package/build/src/accounts/lucid_store/status_profile.js +66 -0
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/define_config.d.ts +53 -0
- package/build/src/define_config.js +14 -1
- package/build/src/doctor/checks.js +32 -32
- package/build/src/events/dispatcher.d.ts +45 -0
- package/build/src/events/dispatcher.js +92 -0
- package/build/src/host/admin_sessions_service.d.ts +8 -0
- package/build/src/host/admin_sessions_service.js +19 -0
- package/build/src/host/controllers/account_apps_controller.d.ts +15 -0
- package/build/src/host/controllers/account_apps_controller.js +61 -0
- package/build/src/host/controllers/account_security_controller.d.ts +9 -0
- package/build/src/host/controllers/account_security_controller.js +52 -2
- package/build/src/host/controllers/account_session_controller.js +3 -1
- package/build/src/host/controllers/admin/admin_users_controller.d.ts +13 -0
- package/build/src/host/controllers/admin/admin_users_controller.js +133 -0
- package/build/src/host/controllers/interaction_controller.d.ts +32 -0
- package/build/src/host/controllers/interaction_controller.js +169 -6
- package/build/src/host/default_mailer.d.ts +8 -0
- package/build/src/host/default_mailer.js +28 -0
- package/build/src/host/i18n.d.ts +90 -0
- package/build/src/host/i18n.js +98 -0
- package/build/src/host/login_attempt.d.ts +1 -0
- package/build/src/host/login_attempt.js +11 -0
- package/build/src/host/register_auth_host.js +18 -1
- package/build/src/host/trusted_device.d.ts +61 -0
- package/build/src/host/trusted_device.js +65 -0
- package/build/src/host/validators.d.ts +35 -0
- package/build/src/host/validators.js +14 -0
- package/package.json +1 -1
|
@@ -197,6 +197,34 @@ export async function sendNewLoginEmail(ctx, data) {
|
|
|
197
197
|
ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar alerta de novo acesso');
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Envia o e-mail de magic link (login passwordless) pelo mailer default do host.
|
|
202
|
+
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
|
203
|
+
*/
|
|
204
|
+
export async function sendMagicLinkEmail(ctx, data) {
|
|
205
|
+
try {
|
|
206
|
+
const brand = resolveBrand(ctx);
|
|
207
|
+
const { messages: t, locale } = resolveMailMessages(ctx);
|
|
208
|
+
const content = renderTransactionalEmail({
|
|
209
|
+
brand,
|
|
210
|
+
locale,
|
|
211
|
+
linkFallback: translate(t, 'mail.common.link_fallback'),
|
|
212
|
+
subject: translate(t, 'mail.magic_link.subject'),
|
|
213
|
+
heading: translate(t, 'mail.magic_link.heading'),
|
|
214
|
+
intro: translate(t, 'mail.magic_link.intro'),
|
|
215
|
+
ctaLabel: translate(t, 'mail.magic_link.cta'),
|
|
216
|
+
ctaUrl: data.magicUrl,
|
|
217
|
+
footnote: translate(t, 'mail.magic_link.fallback'),
|
|
218
|
+
});
|
|
219
|
+
const sent = await sendEmail(ctx, data.email, content);
|
|
220
|
+
if (!sent) {
|
|
221
|
+
ctx.logger.info({ magicUrl: data.magicUrl, email: data.email }, 'authkit: magic link de login (dev — @adonisjs/mail ausente)');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
ctx.logger.error({ err: error, email: data.email }, 'authkit: falha ao enviar magic link de login');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
200
228
|
/**
|
|
201
229
|
* Envia o e-mail de verificação pelo mailer default do host.
|
|
202
230
|
* Best-effort: no fallback (sem mail) loga o link; nunca lança.
|
package/build/src/host/i18n.d.ts
CHANGED
|
@@ -44,6 +44,9 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
44
44
|
'login.switch_account': string;
|
|
45
45
|
'login.password_label': string;
|
|
46
46
|
'login.submit': string;
|
|
47
|
+
'login.magic_link_button': string;
|
|
48
|
+
'login.magic_link_sent': string;
|
|
49
|
+
'login.passkey_button': string;
|
|
47
50
|
'signup.page_title': string;
|
|
48
51
|
'signup.title': string;
|
|
49
52
|
'signup.intro': string;
|
|
@@ -80,6 +83,7 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
80
83
|
'mfa_challenge.recovery_submit': string;
|
|
81
84
|
'mfa_challenge.passkey_button': string;
|
|
82
85
|
'mfa_challenge.passkey_error': string;
|
|
86
|
+
'mfa_challenge.trust_device': string;
|
|
83
87
|
'consent.page_title': string;
|
|
84
88
|
'consent.title': string;
|
|
85
89
|
'consent.body': string;
|
|
@@ -121,6 +125,27 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
121
125
|
'account.security.change_email_submit': string;
|
|
122
126
|
'account.security.email_change_requested': string;
|
|
123
127
|
'account.security.email_changed': string;
|
|
128
|
+
'account.security.trusted_devices_section': string;
|
|
129
|
+
'account.security.trusted_devices_intro': string;
|
|
130
|
+
'account.security.trusted_devices_revoke': string;
|
|
131
|
+
'account.security.trusted_devices_revoked': string;
|
|
132
|
+
'account.profile.section': string;
|
|
133
|
+
'account.profile.intro': string;
|
|
134
|
+
'account.profile.name_label': string;
|
|
135
|
+
'account.profile.avatar_label': string;
|
|
136
|
+
'account.profile.submit': string;
|
|
137
|
+
'account.profile.updated': string;
|
|
138
|
+
'account.profile.not_supported': string;
|
|
139
|
+
'account.apps.page_title': string;
|
|
140
|
+
'account.apps.title': string;
|
|
141
|
+
'account.apps.intro': string;
|
|
142
|
+
'account.apps.logout': string;
|
|
143
|
+
'account.apps.empty': string;
|
|
144
|
+
'account.apps.tokens': string;
|
|
145
|
+
'account.apps.revoke': string;
|
|
146
|
+
'account.apps.revoke_confirm': string;
|
|
147
|
+
'account.apps.revoked': string;
|
|
148
|
+
'account.apps.not_supported': string;
|
|
124
149
|
'account.email_confirmed.page_title': string;
|
|
125
150
|
'account.email_confirmed.ok_title': string;
|
|
126
151
|
'account.email_confirmed.ok_body': string;
|
|
@@ -167,6 +192,20 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
167
192
|
'admin.users.roles_placeholder': string;
|
|
168
193
|
'admin.users.save_roles': string;
|
|
169
194
|
'admin.users.sessions': string;
|
|
195
|
+
'admin.users.create_section': string;
|
|
196
|
+
'admin.users.create_name_placeholder': string;
|
|
197
|
+
'admin.users.create_email_placeholder': string;
|
|
198
|
+
'admin.users.create_password_placeholder': string;
|
|
199
|
+
'admin.users.create_submit': string;
|
|
200
|
+
'admin.users.created': string;
|
|
201
|
+
'admin.users.reset_password': string;
|
|
202
|
+
'admin.users.reset_sent': string;
|
|
203
|
+
'admin.users.disable': string;
|
|
204
|
+
'admin.users.enable': string;
|
|
205
|
+
'admin.users.disabled': string;
|
|
206
|
+
'admin.users.enabled': string;
|
|
207
|
+
'admin.users.disabled_badge': string;
|
|
208
|
+
'admin.users.disable_confirm': string;
|
|
170
209
|
'admin.sessions.page_title': string;
|
|
171
210
|
'admin.sessions.title': string;
|
|
172
211
|
'admin.sessions.account': string;
|
|
@@ -243,6 +282,7 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
243
282
|
'mfa_challenge.required_no_enrollment': string;
|
|
244
283
|
'errors.invalid_credentials': string;
|
|
245
284
|
'errors.invalid_code': string;
|
|
285
|
+
'errors.account_disabled': string;
|
|
246
286
|
'errors.email_taken': string;
|
|
247
287
|
'errors.signup_failed': string;
|
|
248
288
|
'errors.invalid_or_expired_token': string;
|
|
@@ -264,6 +304,11 @@ export declare const DEFAULT_MESSAGES: {
|
|
|
264
304
|
'mail.verify.cta': string;
|
|
265
305
|
'mail.verify.fallback': string;
|
|
266
306
|
'mail.verify.expires': string;
|
|
307
|
+
'mail.magic_link.subject': string;
|
|
308
|
+
'mail.magic_link.heading': string;
|
|
309
|
+
'mail.magic_link.intro': string;
|
|
310
|
+
'mail.magic_link.cta': string;
|
|
311
|
+
'mail.magic_link.fallback': string;
|
|
267
312
|
'mail.new_login.subject': string;
|
|
268
313
|
'mail.new_login.heading': string;
|
|
269
314
|
'mail.new_login.intro': string;
|
|
@@ -298,6 +343,9 @@ export declare const PT_BR_MESSAGES: {
|
|
|
298
343
|
'login.switch_account': string;
|
|
299
344
|
'login.password_label': string;
|
|
300
345
|
'login.submit': string;
|
|
346
|
+
'login.magic_link_button': string;
|
|
347
|
+
'login.magic_link_sent': string;
|
|
348
|
+
'login.passkey_button': string;
|
|
301
349
|
'signup.page_title': string;
|
|
302
350
|
'signup.title': string;
|
|
303
351
|
'signup.intro': string;
|
|
@@ -334,6 +382,7 @@ export declare const PT_BR_MESSAGES: {
|
|
|
334
382
|
'mfa_challenge.recovery_submit': string;
|
|
335
383
|
'mfa_challenge.passkey_button': string;
|
|
336
384
|
'mfa_challenge.passkey_error': string;
|
|
385
|
+
'mfa_challenge.trust_device': string;
|
|
337
386
|
'consent.page_title': string;
|
|
338
387
|
'consent.title': string;
|
|
339
388
|
'consent.body': string;
|
|
@@ -375,6 +424,27 @@ export declare const PT_BR_MESSAGES: {
|
|
|
375
424
|
'account.security.change_email_submit': string;
|
|
376
425
|
'account.security.email_change_requested': string;
|
|
377
426
|
'account.security.email_changed': string;
|
|
427
|
+
'account.security.trusted_devices_section': string;
|
|
428
|
+
'account.security.trusted_devices_intro': string;
|
|
429
|
+
'account.security.trusted_devices_revoke': string;
|
|
430
|
+
'account.security.trusted_devices_revoked': string;
|
|
431
|
+
'account.profile.section': string;
|
|
432
|
+
'account.profile.intro': string;
|
|
433
|
+
'account.profile.name_label': string;
|
|
434
|
+
'account.profile.avatar_label': string;
|
|
435
|
+
'account.profile.submit': string;
|
|
436
|
+
'account.profile.updated': string;
|
|
437
|
+
'account.profile.not_supported': string;
|
|
438
|
+
'account.apps.page_title': string;
|
|
439
|
+
'account.apps.title': string;
|
|
440
|
+
'account.apps.intro': string;
|
|
441
|
+
'account.apps.logout': string;
|
|
442
|
+
'account.apps.empty': string;
|
|
443
|
+
'account.apps.tokens': string;
|
|
444
|
+
'account.apps.revoke': string;
|
|
445
|
+
'account.apps.revoke_confirm': string;
|
|
446
|
+
'account.apps.revoked': string;
|
|
447
|
+
'account.apps.not_supported': string;
|
|
378
448
|
'account.email_confirmed.page_title': string;
|
|
379
449
|
'account.email_confirmed.ok_title': string;
|
|
380
450
|
'account.email_confirmed.ok_body': string;
|
|
@@ -421,6 +491,20 @@ export declare const PT_BR_MESSAGES: {
|
|
|
421
491
|
'admin.users.roles_placeholder': string;
|
|
422
492
|
'admin.users.save_roles': string;
|
|
423
493
|
'admin.users.sessions': string;
|
|
494
|
+
'admin.users.create_section': string;
|
|
495
|
+
'admin.users.create_name_placeholder': string;
|
|
496
|
+
'admin.users.create_email_placeholder': string;
|
|
497
|
+
'admin.users.create_password_placeholder': string;
|
|
498
|
+
'admin.users.create_submit': string;
|
|
499
|
+
'admin.users.created': string;
|
|
500
|
+
'admin.users.reset_password': string;
|
|
501
|
+
'admin.users.reset_sent': string;
|
|
502
|
+
'admin.users.disable': string;
|
|
503
|
+
'admin.users.enable': string;
|
|
504
|
+
'admin.users.disabled': string;
|
|
505
|
+
'admin.users.enabled': string;
|
|
506
|
+
'admin.users.disabled_badge': string;
|
|
507
|
+
'admin.users.disable_confirm': string;
|
|
424
508
|
'admin.sessions.page_title': string;
|
|
425
509
|
'admin.sessions.title': string;
|
|
426
510
|
'admin.sessions.account': string;
|
|
@@ -497,6 +581,7 @@ export declare const PT_BR_MESSAGES: {
|
|
|
497
581
|
'mfa_challenge.required_no_enrollment': string;
|
|
498
582
|
'errors.invalid_credentials': string;
|
|
499
583
|
'errors.invalid_code': string;
|
|
584
|
+
'errors.account_disabled': string;
|
|
500
585
|
'errors.email_taken': string;
|
|
501
586
|
'errors.signup_failed': string;
|
|
502
587
|
'errors.invalid_or_expired_token': string;
|
|
@@ -518,6 +603,11 @@ export declare const PT_BR_MESSAGES: {
|
|
|
518
603
|
'mail.verify.cta': string;
|
|
519
604
|
'mail.verify.fallback': string;
|
|
520
605
|
'mail.verify.expires': string;
|
|
606
|
+
'mail.magic_link.subject': string;
|
|
607
|
+
'mail.magic_link.heading': string;
|
|
608
|
+
'mail.magic_link.intro': string;
|
|
609
|
+
'mail.magic_link.cta': string;
|
|
610
|
+
'mail.magic_link.fallback': string;
|
|
521
611
|
'mail.new_login.subject': string;
|
|
522
612
|
'mail.new_login.heading': string;
|
|
523
613
|
'mail.new_login.intro': string;
|
package/build/src/host/i18n.js
CHANGED
|
@@ -32,6 +32,10 @@ export const DEFAULT_MESSAGES = {
|
|
|
32
32
|
'login.switch_account': 'Switch account',
|
|
33
33
|
'login.password_label': 'Password',
|
|
34
34
|
'login.submit': 'Log in',
|
|
35
|
+
// Passwordless (login).
|
|
36
|
+
'login.magic_link_button': 'Email me a login link',
|
|
37
|
+
'login.magic_link_sent': 'If the account exists, we sent you a login link.',
|
|
38
|
+
'login.passkey_button': 'Sign in with a passkey',
|
|
35
39
|
// Tela de cadastro (signup).
|
|
36
40
|
'signup.page_title': 'Create account',
|
|
37
41
|
'signup.title': 'Create account',
|
|
@@ -73,6 +77,7 @@ export const DEFAULT_MESSAGES = {
|
|
|
73
77
|
'mfa_challenge.recovery_submit': 'Log in with a recovery code',
|
|
74
78
|
'mfa_challenge.passkey_button': 'Use passkey',
|
|
75
79
|
'mfa_challenge.passkey_error': 'Could not authenticate with the passkey. Please try again.',
|
|
80
|
+
'mfa_challenge.trust_device': 'Trust this device for {days} days',
|
|
76
81
|
// Consent (autorização de cliente OIDC).
|
|
77
82
|
'consent.page_title': 'Authorize',
|
|
78
83
|
'consent.title': 'Authorize access',
|
|
@@ -120,6 +125,30 @@ export const DEFAULT_MESSAGES = {
|
|
|
120
125
|
'account.security.change_email_submit': 'Request email change',
|
|
121
126
|
'account.security.email_change_requested': 'We sent a confirmation link to {email}. Click it to complete the change.',
|
|
122
127
|
'account.security.email_changed': 'Email changed successfully.',
|
|
128
|
+
// Trusted devices (account/security).
|
|
129
|
+
'account.security.trusted_devices_section': 'Trusted devices',
|
|
130
|
+
'account.security.trusted_devices_intro': 'You can stop trusting this browser so two-factor is required here again. To revoke trust on all devices, re-enroll your authenticator.',
|
|
131
|
+
'account.security.trusted_devices_revoke': 'Stop trusting this device',
|
|
132
|
+
'account.security.trusted_devices_revoked': 'This device is no longer trusted. Two-factor will be required here again.',
|
|
133
|
+
// Console de conta — perfil (seção em account/security).
|
|
134
|
+
'account.profile.section': 'Profile',
|
|
135
|
+
'account.profile.intro': 'Update your display name and avatar.',
|
|
136
|
+
'account.profile.name_label': 'Name',
|
|
137
|
+
'account.profile.avatar_label': 'Avatar URL',
|
|
138
|
+
'account.profile.submit': 'Save profile',
|
|
139
|
+
'account.profile.updated': 'Profile updated successfully.',
|
|
140
|
+
'account.profile.not_supported': 'Profile editing is not available in this installation.',
|
|
141
|
+
// Console de conta — apps com acesso (account/apps).
|
|
142
|
+
'account.apps.page_title': 'Apps with access',
|
|
143
|
+
'account.apps.title': 'Apps with access',
|
|
144
|
+
'account.apps.intro': 'Apps you have authorized to access your account.',
|
|
145
|
+
'account.apps.logout': 'Log out',
|
|
146
|
+
'account.apps.empty': 'No apps have access to your account.',
|
|
147
|
+
'account.apps.tokens': '{accessTokens} access · {refreshTokens} refresh',
|
|
148
|
+
'account.apps.revoke': 'Revoke access',
|
|
149
|
+
'account.apps.revoke_confirm': 'Revoke this app’s access? It will need to be authorized again and its tokens will stop working.',
|
|
150
|
+
'account.apps.revoked': 'Access revoked.',
|
|
151
|
+
'account.apps.not_supported': 'The configured OIDC adapter does not support enumeration — listing apps is unavailable.',
|
|
123
152
|
// Confirmação de troca de e-mail (account/email-confirmed).
|
|
124
153
|
'account.email_confirmed.page_title': 'Email confirmation',
|
|
125
154
|
'account.email_confirmed.ok_title': 'Email changed',
|
|
@@ -172,6 +201,20 @@ export const DEFAULT_MESSAGES = {
|
|
|
172
201
|
'admin.users.roles_placeholder': 'Roles (comma-separated)',
|
|
173
202
|
'admin.users.save_roles': 'Save roles',
|
|
174
203
|
'admin.users.sessions': 'Sessions',
|
|
204
|
+
'admin.users.create_section': 'Create user',
|
|
205
|
+
'admin.users.create_name_placeholder': 'Name (optional)',
|
|
206
|
+
'admin.users.create_email_placeholder': 'Email',
|
|
207
|
+
'admin.users.create_password_placeholder': 'Password (leave blank to send invite)',
|
|
208
|
+
'admin.users.create_submit': 'Create user',
|
|
209
|
+
'admin.users.created': 'User created.',
|
|
210
|
+
'admin.users.reset_password': 'Send password reset',
|
|
211
|
+
'admin.users.reset_sent': 'Password reset email sent.',
|
|
212
|
+
'admin.users.disable': 'Disable',
|
|
213
|
+
'admin.users.enable': 'Enable',
|
|
214
|
+
'admin.users.disabled': 'Account disabled.',
|
|
215
|
+
'admin.users.enabled': 'Account enabled.',
|
|
216
|
+
'admin.users.disabled_badge': 'Disabled',
|
|
217
|
+
'admin.users.disable_confirm': 'Disable this account? The user will not be able to log in.',
|
|
175
218
|
// Console admin — sessões/grants ativos de uma conta.
|
|
176
219
|
'admin.sessions.page_title': 'Active sessions',
|
|
177
220
|
'admin.sessions.title': 'Active sessions',
|
|
@@ -255,6 +298,7 @@ export const DEFAULT_MESSAGES = {
|
|
|
255
298
|
// Mensagens de erro/flash produzidas pelos controllers.
|
|
256
299
|
'errors.invalid_credentials': 'Invalid credentials',
|
|
257
300
|
'errors.invalid_code': 'Invalid code',
|
|
301
|
+
'errors.account_disabled': 'This account has been disabled.',
|
|
258
302
|
'errors.email_taken': 'Email already registered',
|
|
259
303
|
'errors.signup_failed': 'Could not create the account',
|
|
260
304
|
'errors.invalid_or_expired_token': 'Invalid or expired token',
|
|
@@ -277,6 +321,11 @@ export const DEFAULT_MESSAGES = {
|
|
|
277
321
|
'mail.verify.cta': 'Verify email',
|
|
278
322
|
'mail.verify.fallback': 'If you did not create this account, you can ignore this email.',
|
|
279
323
|
'mail.verify.expires': 'This link expires in {minutes} minutes.',
|
|
324
|
+
'mail.magic_link.subject': 'Your login link',
|
|
325
|
+
'mail.magic_link.heading': 'Sign in to your account',
|
|
326
|
+
'mail.magic_link.intro': 'Click the button below to sign in. The link expires shortly and can be used once.',
|
|
327
|
+
'mail.magic_link.cta': 'Sign in',
|
|
328
|
+
'mail.magic_link.fallback': 'If you did not request this, you can ignore this email.',
|
|
280
329
|
'mail.new_login.subject': 'New login to your account',
|
|
281
330
|
'mail.new_login.heading': 'New login detected',
|
|
282
331
|
'mail.new_login.intro': 'We detected a new login to your account.',
|
|
@@ -313,6 +362,10 @@ export const PT_BR_MESSAGES = {
|
|
|
313
362
|
'login.switch_account': 'Trocar de conta',
|
|
314
363
|
'login.password_label': 'Senha',
|
|
315
364
|
'login.submit': 'Entrar',
|
|
365
|
+
// Passwordless (login).
|
|
366
|
+
'login.magic_link_button': 'Me envie um link de login',
|
|
367
|
+
'login.magic_link_sent': 'Se a conta existir, enviamos um link de login.',
|
|
368
|
+
'login.passkey_button': 'Entrar com passkey',
|
|
316
369
|
// Tela de cadastro (signup).
|
|
317
370
|
'signup.page_title': 'Criar conta',
|
|
318
371
|
'signup.title': 'Criar conta',
|
|
@@ -354,6 +407,7 @@ export const PT_BR_MESSAGES = {
|
|
|
354
407
|
'mfa_challenge.recovery_submit': 'Entrar com código de recuperação',
|
|
355
408
|
'mfa_challenge.passkey_button': 'Usar passkey',
|
|
356
409
|
'mfa_challenge.passkey_error': 'Não foi possível autenticar com a passkey. Tente novamente.',
|
|
410
|
+
'mfa_challenge.trust_device': 'Confiar neste dispositivo por {days} dias',
|
|
357
411
|
// Consent (autorização de cliente OIDC).
|
|
358
412
|
'consent.page_title': 'Autorizar',
|
|
359
413
|
'consent.title': 'Autorizar acesso',
|
|
@@ -399,6 +453,30 @@ export const PT_BR_MESSAGES = {
|
|
|
399
453
|
'account.security.change_email_submit': 'Solicitar troca de e-mail',
|
|
400
454
|
'account.security.email_change_requested': 'Enviamos um link de confirmação para {email}. Clique nele para concluir a troca.',
|
|
401
455
|
'account.security.email_changed': 'E-mail alterado com sucesso.',
|
|
456
|
+
// Trusted devices (account/security).
|
|
457
|
+
'account.security.trusted_devices_section': 'Dispositivos confiáveis',
|
|
458
|
+
'account.security.trusted_devices_intro': 'Você pode deixar de confiar neste navegador para que a verificação em duas etapas volte a ser exigida aqui. Para revogar a confiança em todos os dispositivos, refaça o cadastro do seu autenticador.',
|
|
459
|
+
'account.security.trusted_devices_revoke': 'Deixar de confiar neste dispositivo',
|
|
460
|
+
'account.security.trusted_devices_revoked': 'Este dispositivo não é mais confiável. A verificação em duas etapas voltará a ser exigida aqui.',
|
|
461
|
+
// Console de conta — perfil (seção em account/security).
|
|
462
|
+
'account.profile.section': 'Perfil',
|
|
463
|
+
'account.profile.intro': 'Atualize seu nome de exibição e avatar.',
|
|
464
|
+
'account.profile.name_label': 'Nome',
|
|
465
|
+
'account.profile.avatar_label': 'URL do avatar',
|
|
466
|
+
'account.profile.submit': 'Salvar perfil',
|
|
467
|
+
'account.profile.updated': 'Perfil atualizado com sucesso.',
|
|
468
|
+
'account.profile.not_supported': 'A edição de perfil não está disponível nesta instalação.',
|
|
469
|
+
// Console de conta — apps com acesso (account/apps).
|
|
470
|
+
'account.apps.page_title': 'Apps com acesso',
|
|
471
|
+
'account.apps.title': 'Apps com acesso',
|
|
472
|
+
'account.apps.intro': 'Apps que você autorizou a acessar sua conta.',
|
|
473
|
+
'account.apps.logout': 'Sair',
|
|
474
|
+
'account.apps.empty': 'Nenhum app tem acesso à sua conta.',
|
|
475
|
+
'account.apps.tokens': '{accessTokens} access · {refreshTokens} refresh',
|
|
476
|
+
'account.apps.revoke': 'Revogar acesso',
|
|
477
|
+
'account.apps.revoke_confirm': 'Revogar o acesso deste app? Ele precisará ser autorizado novamente e seus tokens deixarão de funcionar.',
|
|
478
|
+
'account.apps.revoked': 'Acesso revogado.',
|
|
479
|
+
'account.apps.not_supported': 'O adapter OIDC configurado não suporta enumeração — a listagem de apps fica indisponível.',
|
|
402
480
|
// Confirmação de troca de e-mail (account/email-confirmed).
|
|
403
481
|
'account.email_confirmed.page_title': 'Confirmação de e-mail',
|
|
404
482
|
'account.email_confirmed.ok_title': 'E-mail alterado',
|
|
@@ -451,6 +529,20 @@ export const PT_BR_MESSAGES = {
|
|
|
451
529
|
'admin.users.roles_placeholder': 'Papéis (separados por vírgula)',
|
|
452
530
|
'admin.users.save_roles': 'Salvar papéis',
|
|
453
531
|
'admin.users.sessions': 'Sessões',
|
|
532
|
+
'admin.users.create_section': 'Criar usuário',
|
|
533
|
+
'admin.users.create_name_placeholder': 'Nome (opcional)',
|
|
534
|
+
'admin.users.create_email_placeholder': 'E-mail',
|
|
535
|
+
'admin.users.create_password_placeholder': 'Senha (deixe em branco para enviar convite)',
|
|
536
|
+
'admin.users.create_submit': 'Criar usuário',
|
|
537
|
+
'admin.users.created': 'Usuário criado.',
|
|
538
|
+
'admin.users.reset_password': 'Enviar redefinição de senha',
|
|
539
|
+
'admin.users.reset_sent': 'E-mail de redefinição de senha enviado.',
|
|
540
|
+
'admin.users.disable': 'Desabilitar',
|
|
541
|
+
'admin.users.enable': 'Reabilitar',
|
|
542
|
+
'admin.users.disabled': 'Conta desabilitada.',
|
|
543
|
+
'admin.users.enabled': 'Conta reabilitada.',
|
|
544
|
+
'admin.users.disabled_badge': 'Desabilitada',
|
|
545
|
+
'admin.users.disable_confirm': 'Desabilitar esta conta? O usuário não conseguirá entrar.',
|
|
454
546
|
// Console admin — sessões/grants ativos de uma conta.
|
|
455
547
|
'admin.sessions.page_title': 'Sessões ativas',
|
|
456
548
|
'admin.sessions.title': 'Sessões ativas',
|
|
@@ -534,6 +626,7 @@ export const PT_BR_MESSAGES = {
|
|
|
534
626
|
// Mensagens de erro/flash produzidas pelos controllers.
|
|
535
627
|
'errors.invalid_credentials': 'Credenciais inválidas',
|
|
536
628
|
'errors.invalid_code': 'Código inválido',
|
|
629
|
+
'errors.account_disabled': 'Esta conta foi desabilitada.',
|
|
537
630
|
'errors.email_taken': 'E-mail já cadastrado',
|
|
538
631
|
'errors.signup_failed': 'Não foi possível criar a conta',
|
|
539
632
|
'errors.invalid_or_expired_token': 'Token inválido ou expirado',
|
|
@@ -556,6 +649,11 @@ export const PT_BR_MESSAGES = {
|
|
|
556
649
|
'mail.verify.cta': 'Verificar e-mail',
|
|
557
650
|
'mail.verify.fallback': 'Se você não criou esta conta, pode ignorar este e-mail.',
|
|
558
651
|
'mail.verify.expires': 'Este link expira em {minutes} minutos.',
|
|
652
|
+
'mail.magic_link.subject': 'Seu link de login',
|
|
653
|
+
'mail.magic_link.heading': 'Entrar na sua conta',
|
|
654
|
+
'mail.magic_link.intro': 'Clique no botão abaixo para entrar. O link expira em breve e pode ser usado uma vez.',
|
|
655
|
+
'mail.magic_link.cta': 'Entrar',
|
|
656
|
+
'mail.magic_link.fallback': 'Se você não solicitou isso, pode ignorar este e-mail.',
|
|
559
657
|
'mail.new_login.subject': 'Novo login na sua conta',
|
|
560
658
|
'mail.new_login.heading': 'Novo login detectado',
|
|
561
659
|
'mail.new_login.intro': 'Detectamos um novo login na sua conta.',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { supportsAccountStatus } from '../accounts/account_store.js';
|
|
1
2
|
import { createAccountLockout } from './account_lockout.js';
|
|
2
3
|
/**
|
|
3
4
|
* Sequência canônica de login por senha + bloqueio progressivo, compartilhada
|
|
@@ -31,6 +32,16 @@ export async function attemptPasswordLogin(cfg, input) {
|
|
|
31
32
|
await lockout.recordFailure(email, { sink: cfg.audit, ip });
|
|
32
33
|
return { ok: false, locked: false };
|
|
33
34
|
}
|
|
35
|
+
// Conta desabilitada: rejeita o login (mesmo com senha correta). A capacidade é
|
|
36
|
+
// opcional — só checada quando o store a implementa. Emite `login.failure` (com
|
|
37
|
+
// motivo `disabled` no metadata) e NÃO registra falha no lockout (não é tentativa
|
|
38
|
+
// de adivinhar senha).
|
|
39
|
+
if (supportsAccountStatus(cfg.accountStore) && (await cfg.accountStore.isDisabled(account.id))) {
|
|
40
|
+
await cfg.audit?.record(input.clientId !== undefined
|
|
41
|
+
? { type: 'login.failure', email, ip, clientId: input.clientId, metadata: { reason: 'disabled' } }
|
|
42
|
+
: { type: 'login.failure', email, ip, metadata: { reason: 'disabled' } });
|
|
43
|
+
return { ok: false, locked: false, disabled: true };
|
|
44
|
+
}
|
|
34
45
|
// Senha correta: limpa o contador de falhas (o lockout protege a etapa de senha).
|
|
35
46
|
await lockout.clearFailures(email);
|
|
36
47
|
return { ok: true, account };
|
|
@@ -53,6 +53,7 @@ const C = {
|
|
|
53
53
|
accountSession: () => import('./controllers/account_session_controller.js'),
|
|
54
54
|
accountTokens: () => import('./controllers/account_tokens_controller.js'),
|
|
55
55
|
accountSecurity: () => import('./controllers/account_security_controller.js'),
|
|
56
|
+
accountApps: () => import('./controllers/account_apps_controller.js'),
|
|
56
57
|
accountMfa: () => import('./controllers/account_mfa_controller.js'),
|
|
57
58
|
adminDashboard: () => import('./controllers/admin/admin_dashboard_controller.js'),
|
|
58
59
|
adminUsers: () => import('./controllers/admin/admin_users_controller.js'),
|
|
@@ -88,6 +89,9 @@ export function registerAuthHost(router, opts) {
|
|
|
88
89
|
// Passkey como 2º fator alternativo no login (begin/finish; challenge na sessão).
|
|
89
90
|
router.post('/auth/interaction/:uid/passkey/options', [C.interaction, 'passkeyOptions']);
|
|
90
91
|
withLogin(router.post('/auth/interaction/:uid/passkey/verify', [C.interaction, 'passkeyVerify']));
|
|
92
|
+
// Magic link (passwordless): POST emite (throttled), GET consome o token do link.
|
|
93
|
+
withLogin(router.post('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkRequest']));
|
|
94
|
+
router.get('/auth/interaction/:uid/magic', [C.interaction, 'magicLinkConsume']);
|
|
91
95
|
router.post('/auth/interaction/:uid/consent', [C.interaction, 'consent']);
|
|
92
96
|
router.get('/auth/interaction/:uid/switch', [C.interaction, 'switchIdentifier']);
|
|
93
97
|
router.get('/auth/interaction/:uid/signup', [C.registration, 'showSignup']);
|
|
@@ -119,10 +123,19 @@ export function registerAuthHost(router, opts) {
|
|
|
119
123
|
router.get('/account/tokens', [C.accountTokens, 'index']);
|
|
120
124
|
router.post('/account/tokens', [C.accountTokens, 'store']);
|
|
121
125
|
router.post('/account/tokens/:id/revoke', [C.accountTokens, 'destroy']);
|
|
122
|
-
// Segurança da conta: trocar senha + solicitar troca de e-mail.
|
|
126
|
+
// Segurança da conta: trocar senha + solicitar troca de e-mail + perfil.
|
|
123
127
|
router.get('/account/security', [C.accountSecurity, 'index']);
|
|
124
128
|
router.post('/account/security/password', [C.accountSecurity, 'changePassword']);
|
|
125
129
|
router.post('/account/security/email', [C.accountSecurity, 'changeEmail']);
|
|
130
|
+
router.post('/account/security/profile', [C.accountSecurity, 'updateProfile']);
|
|
131
|
+
// Trusted devices: limpa o cookie de confiança DESTE navegador.
|
|
132
|
+
router.post('/account/security/trusted-devices/revoke', [
|
|
133
|
+
C.accountSecurity,
|
|
134
|
+
'revokeTrustedDevices',
|
|
135
|
+
]);
|
|
136
|
+
// Apps com acesso (consentimento): lista os grants da conta + revogação por client.
|
|
137
|
+
router.get('/account/apps', [C.accountApps, 'index']);
|
|
138
|
+
router.post('/account/apps/:clientId/revoke', [C.accountApps, 'revoke']);
|
|
126
139
|
// MFA / TOTP (enrollment, confirmação, disable).
|
|
127
140
|
router.get('/account/mfa', [C.accountMfa, 'index']);
|
|
128
141
|
router.post('/account/mfa/enroll', [C.accountMfa, 'enroll']);
|
|
@@ -140,7 +153,11 @@ export function registerAuthHost(router, opts) {
|
|
|
140
153
|
.group(() => {
|
|
141
154
|
router.get('/admin', [C.adminDashboard, 'index']);
|
|
142
155
|
router.get('/admin/users', [C.adminUsers, 'index']);
|
|
156
|
+
router.post('/admin/users', [C.adminUsers, 'store']);
|
|
143
157
|
router.post('/admin/users/:id/roles', [C.adminUsers, 'updateRoles']);
|
|
158
|
+
router.post('/admin/users/:id/reset-password', [C.adminUsers, 'resetPassword']);
|
|
159
|
+
router.post('/admin/users/:id/disable', [C.adminUsers, 'disable']);
|
|
160
|
+
router.post('/admin/users/:id/enable', [C.adminUsers, 'enable']);
|
|
144
161
|
// Sessões/grants ativos da conta + revogação em massa.
|
|
145
162
|
router.get('/admin/users/:id/sessions', [C.adminSessions, 'index']);
|
|
146
163
|
router.post('/admin/users/:id/revoke-sessions', [C.adminSessions, 'revoke']);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Trusted devices" — pular o 2º fator (MFA) neste dispositivo por N dias.
|
|
3
|
+
*
|
|
4
|
+
* Mecanismo SEM novos requisitos de DB: um cookie httpOnly assinado/encriptado
|
|
5
|
+
* com a appKey do host (via `response.encryptedCookie` / `request.encryptedCookie`,
|
|
6
|
+
* que são appKey-backed). O cookie carrega `{ a: accountId, d: deviceId, iat, exp }`.
|
|
7
|
+
*
|
|
8
|
+
* Validação (ver {@link isTrustedDeviceValid}):
|
|
9
|
+
* - `exp` ainda no futuro;
|
|
10
|
+
* - `a` casa com a conta que acabou de passar pela senha;
|
|
11
|
+
* - `iat >= mfaEnabledAt` — re-enrolar o MFA invalida cookies antigos (revogação
|
|
12
|
+
* por re-enrollment, sem estado server-side).
|
|
13
|
+
*
|
|
14
|
+
* Step-up (acr_values pedindo o mfaAcr) SEMPRE ignora o cookie e força o MFA — a
|
|
15
|
+
* decisão fica no controller, antes de checar o cookie.
|
|
16
|
+
*
|
|
17
|
+
* Limitação conhecida (documentada): NÃO há uma lista de revogação por-dispositivo
|
|
18
|
+
* server-side; a revogação disponível é "revogar todos" via re-enrollment do MFA.
|
|
19
|
+
* Uma allowlist/denylist persistida fica como trabalho futuro.
|
|
20
|
+
*/
|
|
21
|
+
/** Nome do cookie de dispositivo confiável. */
|
|
22
|
+
export declare const TRUSTED_DEVICE_COOKIE = "authkit_trusted_device";
|
|
23
|
+
/** Payload guardado (encriptado) no cookie de dispositivo confiável. */
|
|
24
|
+
export interface TrustedDevicePayload {
|
|
25
|
+
/** accountId ao qual a confiança pertence. */
|
|
26
|
+
a: string;
|
|
27
|
+
/** id opaco do dispositivo (para futura revogação por-dispositivo). */
|
|
28
|
+
d: string;
|
|
29
|
+
/** issued-at (epoch ms). */
|
|
30
|
+
iat: number;
|
|
31
|
+
/** expiry (epoch ms). */
|
|
32
|
+
exp: number;
|
|
33
|
+
}
|
|
34
|
+
export interface TrustedDevicesConfigInput {
|
|
35
|
+
/** Liga o mecanismo de trusted devices. Default: true. */
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
/** Validade da confiança em dias. Default: 30. */
|
|
38
|
+
days?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ResolvedTrustedDevicesConfig {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
days: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function resolveTrustedDevices(input?: TrustedDevicesConfigInput): ResolvedTrustedDevicesConfig;
|
|
45
|
+
/** Constrói o payload de um novo cookie de confiança para a conta. */
|
|
46
|
+
export declare function buildTrustedDevicePayload(accountId: string, cfg: ResolvedTrustedDevicesConfig, now?: number): TrustedDevicePayload;
|
|
47
|
+
/**
|
|
48
|
+
* `true` se o payload do cookie é uma confiança VÁLIDA para `accountId`:
|
|
49
|
+
* - estrutura íntegra;
|
|
50
|
+
* - pertence à conta certa;
|
|
51
|
+
* - não expirou;
|
|
52
|
+
* - foi emitido em/depois do último (re)enrollment de MFA (`mfaEnabledAt`).
|
|
53
|
+
*
|
|
54
|
+
* `mfaEnabledAt` em epoch ms (ou null quando o store não rastreia — nesse caso a
|
|
55
|
+
* checagem de re-enrollment é pulada, mantendo a validade por expiração apenas).
|
|
56
|
+
*/
|
|
57
|
+
export declare function isTrustedDeviceValid(payload: unknown, opts: {
|
|
58
|
+
accountId: string;
|
|
59
|
+
mfaEnabledAt?: number | null;
|
|
60
|
+
now?: number;
|
|
61
|
+
}): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* "Trusted devices" — pular o 2º fator (MFA) neste dispositivo por N dias.
|
|
4
|
+
*
|
|
5
|
+
* Mecanismo SEM novos requisitos de DB: um cookie httpOnly assinado/encriptado
|
|
6
|
+
* com a appKey do host (via `response.encryptedCookie` / `request.encryptedCookie`,
|
|
7
|
+
* que são appKey-backed). O cookie carrega `{ a: accountId, d: deviceId, iat, exp }`.
|
|
8
|
+
*
|
|
9
|
+
* Validação (ver {@link isTrustedDeviceValid}):
|
|
10
|
+
* - `exp` ainda no futuro;
|
|
11
|
+
* - `a` casa com a conta que acabou de passar pela senha;
|
|
12
|
+
* - `iat >= mfaEnabledAt` — re-enrolar o MFA invalida cookies antigos (revogação
|
|
13
|
+
* por re-enrollment, sem estado server-side).
|
|
14
|
+
*
|
|
15
|
+
* Step-up (acr_values pedindo o mfaAcr) SEMPRE ignora o cookie e força o MFA — a
|
|
16
|
+
* decisão fica no controller, antes de checar o cookie.
|
|
17
|
+
*
|
|
18
|
+
* Limitação conhecida (documentada): NÃO há uma lista de revogação por-dispositivo
|
|
19
|
+
* server-side; a revogação disponível é "revogar todos" via re-enrollment do MFA.
|
|
20
|
+
* Uma allowlist/denylist persistida fica como trabalho futuro.
|
|
21
|
+
*/
|
|
22
|
+
/** Nome do cookie de dispositivo confiável. */
|
|
23
|
+
export const TRUSTED_DEVICE_COOKIE = 'authkit_trusted_device';
|
|
24
|
+
export function resolveTrustedDevices(input) {
|
|
25
|
+
return {
|
|
26
|
+
enabled: input?.enabled ?? true,
|
|
27
|
+
days: input?.days && input.days > 0 ? input.days : 30,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Constrói o payload de um novo cookie de confiança para a conta. */
|
|
31
|
+
export function buildTrustedDevicePayload(accountId, cfg, now = Date.now()) {
|
|
32
|
+
return {
|
|
33
|
+
a: accountId,
|
|
34
|
+
d: randomBytes(16).toString('hex'),
|
|
35
|
+
iat: now,
|
|
36
|
+
exp: now + cfg.days * 24 * 60 * 60 * 1000,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `true` se o payload do cookie é uma confiança VÁLIDA para `accountId`:
|
|
41
|
+
* - estrutura íntegra;
|
|
42
|
+
* - pertence à conta certa;
|
|
43
|
+
* - não expirou;
|
|
44
|
+
* - foi emitido em/depois do último (re)enrollment de MFA (`mfaEnabledAt`).
|
|
45
|
+
*
|
|
46
|
+
* `mfaEnabledAt` em epoch ms (ou null quando o store não rastreia — nesse caso a
|
|
47
|
+
* checagem de re-enrollment é pulada, mantendo a validade por expiração apenas).
|
|
48
|
+
*/
|
|
49
|
+
export function isTrustedDeviceValid(payload, opts) {
|
|
50
|
+
const now = opts.now ?? Date.now();
|
|
51
|
+
if (!payload || typeof payload !== 'object')
|
|
52
|
+
return false;
|
|
53
|
+
const p = payload;
|
|
54
|
+
if (typeof p.a !== 'string' || typeof p.iat !== 'number' || typeof p.exp !== 'number') {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (p.a !== opts.accountId)
|
|
58
|
+
return false;
|
|
59
|
+
if (p.exp <= now)
|
|
60
|
+
return false;
|
|
61
|
+
// Re-enrollment do MFA revoga cookies emitidos antes dele.
|
|
62
|
+
if (typeof opts.mfaEnabledAt === 'number' && p.iat < opts.mfaEnabledAt)
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
@@ -69,3 +69,38 @@ export declare const changeEmailValidator: import("@vinejs/vine").VineValidator<
|
|
|
69
69
|
newEmail: string;
|
|
70
70
|
currentPassword: string;
|
|
71
71
|
}>, Record<string, any> | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* Edição de perfil no console de conta: nome e avatarUrl, ambos opcionais.
|
|
74
|
+
* Campos vazios são normalizados para string vazia (limpa o valor).
|
|
75
|
+
*/
|
|
76
|
+
export declare const updateProfileValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
|
|
77
|
+
name: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
|
|
78
|
+
avatarUrl: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
|
|
79
|
+
}, {
|
|
80
|
+
name?: string | null | undefined;
|
|
81
|
+
avatarUrl?: string | null | undefined;
|
|
82
|
+
}, {
|
|
83
|
+
name?: string | undefined;
|
|
84
|
+
avatarUrl?: string | undefined;
|
|
85
|
+
}, {
|
|
86
|
+
name?: string | undefined;
|
|
87
|
+
avatarUrl?: string | undefined;
|
|
88
|
+
}>, Record<string, any> | undefined>;
|
|
89
|
+
/** Criação de usuário no console admin (email obrigatório; nome/senha opcionais). */
|
|
90
|
+
export declare const adminCreateUserValidator: import("@vinejs/vine").VineValidator<import("@vinejs/vine").VineObject<{
|
|
91
|
+
email: import("@vinejs/vine").VineString;
|
|
92
|
+
name: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
|
|
93
|
+
password: import("@vinejs/vine/schema/base/literal").OptionalModifier<import("@vinejs/vine").VineString>;
|
|
94
|
+
}, {
|
|
95
|
+
password?: string | null | undefined;
|
|
96
|
+
name?: string | null | undefined;
|
|
97
|
+
email: string;
|
|
98
|
+
}, {
|
|
99
|
+
password?: string | undefined;
|
|
100
|
+
name?: string | undefined;
|
|
101
|
+
email: string;
|
|
102
|
+
}, {
|
|
103
|
+
password?: string | undefined;
|
|
104
|
+
name?: string | undefined;
|
|
105
|
+
email: string;
|
|
106
|
+
}>, Record<string, any> | undefined>;
|
|
@@ -25,3 +25,17 @@ export const changeEmailValidator = vine.compile(vine.object({
|
|
|
25
25
|
currentPassword: vine.string().minLength(1),
|
|
26
26
|
newEmail: vine.string().trim().email().normalizeEmail(),
|
|
27
27
|
}));
|
|
28
|
+
/**
|
|
29
|
+
* Edição de perfil no console de conta: nome e avatarUrl, ambos opcionais.
|
|
30
|
+
* Campos vazios são normalizados para string vazia (limpa o valor).
|
|
31
|
+
*/
|
|
32
|
+
export const updateProfileValidator = vine.compile(vine.object({
|
|
33
|
+
name: vine.string().trim().maxLength(255).optional(),
|
|
34
|
+
avatarUrl: vine.string().trim().url().maxLength(2048).optional(),
|
|
35
|
+
}));
|
|
36
|
+
/** Criação de usuário no console admin (email obrigatório; nome/senha opcionais). */
|
|
37
|
+
export const adminCreateUserValidator = vine.compile(vine.object({
|
|
38
|
+
email: vine.string().trim().email().normalizeEmail(),
|
|
39
|
+
name: vine.string().trim().maxLength(255).optional(),
|
|
40
|
+
password: vine.string().minLength(8).maxLength(255).optional(),
|
|
41
|
+
}));
|