@dudousxd/adonis-authkit-server 0.3.0 → 0.5.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.
Files changed (58) hide show
  1. package/build/commands/commands.json +28 -0
  2. package/build/commands/doctor.d.ts +10 -0
  3. package/build/commands/doctor.js +66 -0
  4. package/build/commands/rotate_keys.d.ts +10 -0
  5. package/build/commands/rotate_keys.js +53 -0
  6. package/build/host/views/account/email-confirmed.edge +15 -0
  7. package/build/host/views/account/security.edge +83 -0
  8. package/build/host/views/account/tokens.edge +7 -4
  9. package/build/host/views/admin/sessions.edge +89 -0
  10. package/build/host/views/admin/users.edge +1 -0
  11. package/build/host/views/mfa-challenge.edge +29 -23
  12. package/build/index.d.ts +5 -4
  13. package/build/index.js +3 -3
  14. package/build/src/accounts/account_store.d.ts +46 -1
  15. package/build/src/accounts/account_store.js +4 -0
  16. package/build/src/accounts/lucid_store/core.d.ts +5 -4
  17. package/build/src/accounts/lucid_store/core.js +67 -2
  18. package/build/src/adapters/adapter_contract.d.ts +17 -0
  19. package/build/src/adapters/database_adapter.d.ts +9 -5
  20. package/build/src/adapters/database_adapter.js +13 -6
  21. package/build/src/adapters/redis_adapter.d.ts +11 -5
  22. package/build/src/adapters/redis_adapter.js +16 -7
  23. package/build/src/audit/audit_sink.d.ts +1 -1
  24. package/build/src/define_config.d.ts +102 -0
  25. package/build/src/define_config.js +46 -3
  26. package/build/src/doctor/checks.d.ts +51 -0
  27. package/build/src/doctor/checks.js +231 -0
  28. package/build/src/host/admin_clients_service.js +12 -5
  29. package/build/src/host/admin_sessions_service.d.ts +63 -0
  30. package/build/src/host/admin_sessions_service.js +127 -0
  31. package/build/src/host/controllers/account_mfa_controller.js +6 -2
  32. package/build/src/host/controllers/account_security_controller.d.ts +16 -0
  33. package/build/src/host/controllers/account_security_controller.js +119 -0
  34. package/build/src/host/controllers/account_session_controller.js +2 -1
  35. package/build/src/host/controllers/admin/admin_sessions_controller.d.ts +14 -0
  36. package/build/src/host/controllers/admin/admin_sessions_controller.js +64 -0
  37. package/build/src/host/controllers/interaction_controller.d.ts +11 -0
  38. package/build/src/host/controllers/interaction_controller.js +55 -12
  39. package/build/src/host/default_mailer.d.ts +17 -0
  40. package/build/src/host/default_mailer.js +94 -9
  41. package/build/src/host/email_templates.d.ts +4 -0
  42. package/build/src/host/email_templates.js +5 -2
  43. package/build/src/host/i18n.d.ts +358 -11
  44. package/build/src/host/i18n.js +393 -12
  45. package/build/src/host/login_notify.d.ts +20 -0
  46. package/build/src/host/login_notify.js +71 -0
  47. package/build/src/host/register_auth_host.js +12 -0
  48. package/build/src/host/validators.d.ts +32 -0
  49. package/build/src/host/validators.js +14 -0
  50. package/build/src/keys/keystore.d.ts +43 -0
  51. package/build/src/keys/keystore.js +74 -0
  52. package/build/src/observability/metrics_controller.js +4 -4
  53. package/build/src/provider/build_provider.js +23 -0
  54. package/build/src/provider/device_sources.d.ts +6 -0
  55. package/build/src/provider/device_sources.js +65 -0
  56. package/build/src/provider/interaction_actions.d.ts +6 -1
  57. package/build/src/provider/interaction_actions.js +9 -2
  58. package/package.json +2 -2
@@ -25,6 +25,34 @@
25
25
  "args": [],
26
26
  "options": {},
27
27
  "filePath": "eject.js"
28
+ },
29
+ {
30
+ "commandName": "authkit:doctor",
31
+ "description": "Valida a configuração do AuthKit no host e imprime achados (✅/⚠️/❌). Sai com código !=0 se houver erros.",
32
+ "namespace": "authkit",
33
+ "aliases": [],
34
+ "flags": [],
35
+ "args": [],
36
+ "options": { "startApp": true },
37
+ "filePath": "doctor.js"
38
+ },
39
+ {
40
+ "commandName": "authkit:rotate-keys",
41
+ "description": "Rotaciona as chaves de assinatura JWKS managed: gera uma nova chave (novo kid), mantém as N anteriores no JWKS para validar tokens antigos.",
42
+ "namespace": "authkit",
43
+ "aliases": [],
44
+ "flags": [
45
+ {
46
+ "name": "keep",
47
+ "flagName": "keep",
48
+ "required": false,
49
+ "type": "number",
50
+ "description": "Quantas chaves manter no JWKS (default 2)."
51
+ }
52
+ ],
53
+ "args": [],
54
+ "options": { "startApp": true },
55
+ "filePath": "rotate_keys.js"
28
56
  }
29
57
  ]
30
58
  }
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ import type { CommandOptions } from '@adonisjs/core/types/ace';
3
+ export default class AuthkitDoctor extends BaseCommand {
4
+ static commandName: string;
5
+ static description: string;
6
+ static help: string[];
7
+ static options: CommandOptions;
8
+ run(): Promise<void>;
9
+ private print;
10
+ }
@@ -0,0 +1,66 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { BaseCommand } from '@adonisjs/core/ace';
10
+ import { runAllChecks, hasErrors } from '../src/doctor/checks.js';
11
+ /** Tenta importar um peer; true se importável. */
12
+ async function canImport(specifier) {
13
+ try {
14
+ await import(__rewriteRelativeImportExtension(specifier));
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ export default class AuthkitDoctor extends BaseCommand {
22
+ static commandName = 'authkit:doctor';
23
+ static description = 'Valida a configuração do AuthKit no host e imprime achados (✅/⚠️/❌). Sai com código !=0 se houver erros.';
24
+ static help = [
25
+ 'Roda uma bateria de checagens sobre a config `authkit` do host:',
26
+ 'issuer/mountPath, clients, accountStore + capacidades, session, shield,',
27
+ 'ally (social), rate-limit, admin, webauthn e jwks.',
28
+ ];
29
+ static options = { startApp: true };
30
+ async run() {
31
+ const config = await this.app.container.make('config');
32
+ const authkitConfig = config.get('authkit', null) ?? null;
33
+ const sessionConfig = config.get('session', null) ?? null;
34
+ const input = {
35
+ authkitConfig,
36
+ sessionConfig,
37
+ peers: {
38
+ session: await canImport('@adonisjs/session'),
39
+ shield: await canImport('@adonisjs/shield'),
40
+ ally: await canImport('@adonisjs/ally'),
41
+ limiter: await canImport('@adonisjs/limiter'),
42
+ },
43
+ };
44
+ const findings = runAllChecks(input);
45
+ this.print(findings);
46
+ if (hasErrors(findings)) {
47
+ this.exitCode = 1;
48
+ }
49
+ }
50
+ print(findings) {
51
+ const icon = (l) => (l === 'ok' ? '✅' : l === 'warn' ? '⚠️ ' : '❌');
52
+ this.logger.info('AuthKit doctor — checagem da configuração do host\n');
53
+ for (const f of findings) {
54
+ const line = `${icon(f.level)} ${f.message}`;
55
+ if (f.level === 'error')
56
+ this.logger.logError(line);
57
+ else if (f.level === 'warn')
58
+ this.logger.warning(line);
59
+ else
60
+ this.logger.success(line);
61
+ }
62
+ const errors = findings.filter((f) => f.level === 'error').length;
63
+ const warns = findings.filter((f) => f.level === 'warn').length;
64
+ this.logger.info(`\nResumo: ${errors} erro(s), ${warns} aviso(s).`);
65
+ }
66
+ }
@@ -0,0 +1,10 @@
1
+ import { BaseCommand } from '@adonisjs/core/ace';
2
+ import type { CommandOptions } from '@adonisjs/core/types/ace';
3
+ export default class AuthkitRotateKeys extends BaseCommand {
4
+ static commandName: string;
5
+ static description: string;
6
+ static help: string[];
7
+ static options: CommandOptions;
8
+ keep?: number;
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,53 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { BaseCommand, flags } from '@adonisjs/core/ace';
8
+ import { rotateKeystore } from '../src/keys/keystore.js';
9
+ export default class AuthkitRotateKeys extends BaseCommand {
10
+ static commandName = 'authkit:rotate-keys';
11
+ static description = 'Rotaciona as chaves de assinatura JWKS managed: gera uma nova chave (novo kid), mantém as N anteriores no JWKS para validar tokens antigos.';
12
+ static help = [
13
+ 'Requer `jwks: { source: "managed", store: "<arquivo>" }` na config authkit.',
14
+ 'A chave nova vira a de assinatura corrente; as `--keep` mais recentes são',
15
+ 'preservadas no JWKS público para que tokens emitidos antes ainda validem.',
16
+ ];
17
+ static options = { startApp: true };
18
+ async run() {
19
+ const config = await this.app.container.make('config');
20
+ const authkitConfig = config.get('authkit', null);
21
+ if (!authkitConfig?.jwks) {
22
+ this.logger.logError("❌ config('authkit').jwks ausente.");
23
+ this.exitCode = 1;
24
+ return;
25
+ }
26
+ const { source, store, algorithm } = authkitConfig.jwks;
27
+ if (source !== 'managed') {
28
+ this.logger.logError('❌ Rotação só se aplica a jwks.source = "managed".');
29
+ this.exitCode = 1;
30
+ return;
31
+ }
32
+ if (!store) {
33
+ this.logger.logError('❌ jwks.store não configurado. A rotação exige um keystore persistido em arquivo ' +
34
+ '(ex.: jwks: { source: "managed", store: "tmp/authkit_jwks.json" }). ' +
35
+ 'Sem store, o modo managed gera uma chave efêmera por boot e rotacionar não tem efeito.');
36
+ this.exitCode = 1;
37
+ return;
38
+ }
39
+ const storePath = this.app.makePath(store);
40
+ const alg = algorithm ?? 'RS256';
41
+ const keep = this.keep ?? 2;
42
+ const { newKid, retiredKids, store: result } = await rotateKeystore(storePath, alg, keep);
43
+ this.logger.success(`✅ Nova chave de assinatura gerada: kid=${newKid} (alg=${alg}).`);
44
+ this.logger.info(`JWKS agora serve ${result.keys.length} chave(s) (kids: ${result.keys.map((k) => k.kid).join(', ')}).`);
45
+ if (retiredKids.length) {
46
+ this.logger.warning(`⚠️ Chaves aposentadas (tokens assinados por elas deixarão de validar): ${retiredKids.join(', ')}`);
47
+ }
48
+ this.logger.info('Reinicie o processo (ou recarregue a config) para passar a assinar com a nova chave.');
49
+ }
50
+ }
51
+ __decorate([
52
+ flags.number({ description: 'Quantas chaves manter no JWKS (default 2).' })
53
+ ], AuthkitRotateKeys.prototype, "keep", void 0);
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('account.email_confirmed.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen flex items-center justify-center bg-gray-100 p-4">
5
+ <div class="w-full max-w-sm rounded-2xl bg-white p-8 text-center shadow-xl ring-1 ring-black/5">
6
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
7
+ @if(ok)
8
+ <h1 class="mt-2 text-xl font-semibold text-emerald-700">{{ t('account.email_confirmed.ok_title') }}</h1>
9
+ <p class="mt-2 text-sm text-gray-600">{{ t('account.email_confirmed.ok_body') }}</p>
10
+ @else
11
+ <h1 class="mt-2 text-xl font-semibold text-red-700">{{ t('account.email_confirmed.invalid_title') }}</h1>
12
+ <p class="mt-2 text-sm text-gray-600">{{ t('account.email_confirmed.invalid_body') }}</p>
13
+ @end
14
+ </div>
15
+ </body></html>
@@ -0,0 +1,83 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('account.security.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen bg-gray-100 p-4">
5
+ <div class="mx-auto max-w-2xl">
6
+ <div class="flex items-center justify-between py-6">
7
+ <div>
8
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('account.security.title') }}</h1>
10
+ @if(email)
11
+ <p class="mt-1 text-sm text-gray-500">{{ t('account.security.current_email', { email: email }) }}</p>
12
+ @end
13
+ </div>
14
+ <form method="POST" action="/account/logout">
15
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
16
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('account.security.logout') }}</button>
17
+ </form>
18
+ </div>
19
+
20
+ @if(!supported)
21
+ <div class="rounded-xl bg-white p-6 text-sm text-gray-500 shadow-sm ring-1 ring-black/5">
22
+ {{ t('account.security.not_supported') }}
23
+ </div>
24
+ @else
25
+ @if(error)
26
+ <div class="mb-6 rounded-lg border border-red-300 bg-red-50 p-4">
27
+ <p class="text-sm font-medium text-red-900">{{ error }}</p>
28
+ </div>
29
+ @end
30
+ @if(passwordChanged)
31
+ <div class="mb-6 rounded-lg border border-emerald-300 bg-emerald-50 p-4">
32
+ <p class="text-sm font-medium text-emerald-900">{{ passwordChanged }}</p>
33
+ </div>
34
+ @end
35
+ @if(emailChangeRequested)
36
+ <div class="mb-6 rounded-lg border border-blue-300 bg-blue-50 p-4">
37
+ <p class="text-sm font-medium text-blue-900">{{ emailChangeRequested }}</p>
38
+ </div>
39
+ @end
40
+
41
+ <div class="mb-6 rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
42
+ <h2 class="mb-4 text-sm font-semibold text-gray-900">{{ t('account.security.password_section') }}</h2>
43
+ <form method="POST" action="/account/security/password" class="space-y-4">
44
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
45
+ <div>
46
+ <label class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.security.current_password_label') }}</label>
47
+ <input name="currentPassword" type="password" required
48
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
49
+ </div>
50
+ <div>
51
+ <label class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.security.new_password_label') }}</label>
52
+ <input name="newPassword" type="password" required minlength="8"
53
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
54
+ </div>
55
+ <button type="submit" class="rounded-lg bg-gray-900 px-4 py-2 text-sm font-semibold text-white">
56
+ {{ t('account.security.change_password_submit') }}
57
+ </button>
58
+ </form>
59
+ </div>
60
+
61
+ <div class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5">
62
+ <h2 class="mb-1 text-sm font-semibold text-gray-900">{{ t('account.security.email_section') }}</h2>
63
+ <p class="mb-4 text-xs text-gray-500">{{ t('account.security.email_intro') }}</p>
64
+ <form method="POST" action="/account/security/email" class="space-y-4">
65
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
66
+ <div>
67
+ <label class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.security.new_email_label') }}</label>
68
+ <input name="newEmail" type="email" required
69
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
70
+ </div>
71
+ <div>
72
+ <label class="mb-1 block text-sm font-medium text-gray-700">{{ t('account.security.email_password_label') }}</label>
73
+ <input name="currentPassword" type="password" required
74
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
75
+ </div>
76
+ <button type="submit" class="rounded-lg bg-gray-900 px-4 py-2 text-sm font-semibold text-white">
77
+ {{ t('account.security.change_email_submit') }}
78
+ </button>
79
+ </form>
80
+ </div>
81
+ @end
82
+ </div>
83
+ </body></html>
@@ -8,10 +8,13 @@
8
8
  <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
9
  <h1 class="text-xl font-semibold text-gray-900">{{ t('account.tokens.title') }}</h1>
10
10
  </div>
11
- <form method="POST" action="/account/logout">
12
- <input type="hidden" name="_csrf" value="{{ csrfToken }}">
13
- <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('account.tokens.logout') }}</button>
14
- </form>
11
+ <div class="flex items-center gap-4">
12
+ <a href="/account/security" class="text-sm text-gray-500 hover:underline">{{ t('account.tokens.security') }}</a>
13
+ <form method="POST" action="/account/logout">
14
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
15
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('account.tokens.logout') }}</button>
16
+ </form>
17
+ </div>
15
18
  </div>
16
19
 
17
20
  @if(createdToken)
@@ -0,0 +1,89 @@
1
+ <!doctype html>
2
+ <html lang="pt-br"><head><meta charset="utf-8"><title>{{ t('admin.sessions.page_title') }}</title>
3
+ <script src="https://cdn.tailwindcss.com"></script></head>
4
+ <body class="min-h-screen bg-gray-100 p-4">
5
+ <div class="mx-auto max-w-4xl">
6
+ <div class="flex items-center justify-between py-6">
7
+ <div>
8
+ <div class="text-xs font-semibold uppercase tracking-widest text-gray-400">{{ t('common.brand_eyebrow') }}</div>
9
+ <h1 class="text-xl font-semibold text-gray-900">{{ t('admin.sessions.title') }}</h1>
10
+ @if(email)
11
+ <p class="mt-1 text-sm text-gray-500">{{ t('admin.sessions.account', { email: email }) }}</p>
12
+ @end
13
+ </div>
14
+ <form method="POST" action="/account/logout">
15
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
16
+ <button type="submit" class="text-sm text-gray-500 hover:underline">{{ t('admin.nav.logout') }}</button>
17
+ </form>
18
+ </div>
19
+
20
+ <nav class="mb-6 flex gap-4 text-sm font-medium">
21
+ <a href="/admin" class="text-gray-500 hover:underline">{{ t('admin.nav.dashboard') }}</a>
22
+ <a href="/admin/users" class="text-gray-900 underline">{{ t('admin.nav.users') }}</a>
23
+ <a href="/admin/clients" class="text-gray-500 hover:underline">{{ t('admin.nav.clients') }}</a>
24
+ <a href="/admin/audit" class="text-gray-500 hover:underline">{{ t('admin.nav.audit') }}</a>
25
+ </nav>
26
+
27
+ <a href="/admin/users" class="mb-4 inline-block text-sm text-gray-500 hover:underline">&larr; {{ t('admin.sessions.back') }}</a>
28
+
29
+ @if(!supported)
30
+ <div class="rounded-xl bg-white p-6 text-sm text-gray-500 shadow-sm ring-1 ring-black/5">
31
+ {{ t('admin.sessions.not_supported') }}
32
+ </div>
33
+ @else
34
+ @if(revoked)
35
+ <div class="mb-6 rounded-lg border border-emerald-300 bg-emerald-50 p-4">
36
+ <p class="text-sm font-medium text-emerald-900">
37
+ {{ t('admin.sessions.revoked_notice', { sessions: revoked.sessions, grants: revoked.grants, accessTokens: revoked.accessTokens, refreshTokens: revoked.refreshTokens }) }}
38
+ </p>
39
+ </div>
40
+ @end
41
+
42
+ <h2 class="mb-2 text-sm font-semibold text-gray-700">{{ t('admin.sessions.sessions_section') }}</h2>
43
+ <div class="mb-6 overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
44
+ @if(sessions.length === 0)
45
+ <p class="p-6 text-sm text-gray-500">{{ t('admin.sessions.sessions_empty') }}</p>
46
+ @else
47
+ @each(session in sessions)
48
+ <div class="border-b border-gray-100 p-4 last:border-0">
49
+ <p class="text-sm font-medium text-gray-900">{{ session.id }}</p>
50
+ @if(session.loginTs)
51
+ <p class="text-xs text-gray-500">{{ t('admin.sessions.session_login_ts', { date: session.loginTs }) }}</p>
52
+ @end
53
+ @if(session.amr)
54
+ <p class="text-xs text-gray-400">{{ t('admin.sessions.session_amr', { amr: session.amr }) }}</p>
55
+ @end
56
+ </div>
57
+ @end
58
+ @end
59
+ </div>
60
+
61
+ <h2 class="mb-2 text-sm font-semibold text-gray-700">{{ t('admin.sessions.grants_section') }}</h2>
62
+ <div class="mb-6 overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
63
+ @if(grants.length === 0)
64
+ <p class="p-6 text-sm text-gray-500">{{ t('admin.sessions.grants_empty') }}</p>
65
+ @else
66
+ @each(grant in grants)
67
+ <div class="border-b border-gray-100 p-4 last:border-0">
68
+ <p class="text-sm font-medium text-gray-900">{{ grant.id }}</p>
69
+ @if(grant.clientId)
70
+ <p class="text-xs text-gray-500">{{ t('admin.sessions.grant_client', { clientId: grant.clientId }) }}</p>
71
+ @end
72
+ <p class="text-xs text-gray-400">{{ t('admin.sessions.grant_tokens', { accessTokens: grant.accessTokens, refreshTokens: grant.refreshTokens }) }}</p>
73
+ </div>
74
+ @end
75
+ @end
76
+ </div>
77
+
78
+ @if(sessions.length > 0 || grants.length > 0)
79
+ <form method="POST" action="/admin/users/{{ accountId }}/revoke-sessions"
80
+ onsubmit="return confirm('{{ t('admin.sessions.revoke_confirm') }}')">
81
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
82
+ <button type="submit" class="rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:opacity-90">
83
+ {{ t('admin.sessions.revoke_all') }}
84
+ </button>
85
+ </form>
86
+ @end
87
+ @end
88
+ </div>
89
+ </body></html>
@@ -42,6 +42,7 @@
42
42
  <p class="text-xs text-gray-500">{{ user.name }}</p>
43
43
  @end
44
44
  </div>
45
+ <a href="/admin/users/{{ user.id }}/sessions" class="text-sm text-gray-500 hover:underline">{{ t('admin.users.sessions') }}</a>
45
46
  </div>
46
47
  <form method="POST" action="/admin/users/{{ user.id }}/roles" class="mt-3 flex gap-2">
47
48
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
@@ -14,20 +14,24 @@
14
14
  <p class="mt-4 text-sm text-red-600">{{ error }}</p>
15
15
  @end
16
16
 
17
- <div class="mt-6">
18
- <label for="code" class="mb-1 block text-sm font-medium text-gray-700">{{ t('mfa_challenge.code_label') }}</label>
19
- <input id="code" name="code" inputmode="numeric" autocomplete="one-time-code"
20
- pattern="[0-9]*" maxlength="6" autofocus
21
- class="w-full rounded-lg border border-gray-300 px-3 py-2 text-center text-lg tracking-[0.4em] outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
22
- </div>
17
+ {{-- Step-up sem MFA enrolado: bloqueia o login e instrui a configurar o MFA;
18
+ não renderiza o campo de código (não há 2º fator a desafiar). --}}
19
+ @if(!noEnrollment)
20
+ <div class="mt-6">
21
+ <label for="code" class="mb-1 block text-sm font-medium text-gray-700">{{ t('mfa_challenge.code_label') }}</label>
22
+ <input id="code" name="code" inputmode="numeric" autocomplete="one-time-code"
23
+ pattern="[0-9]*" maxlength="6" autofocus
24
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-center text-lg tracking-[0.4em] outline-none transition focus:border-gray-900 focus:ring-2 focus:ring-gray-900" />
25
+ </div>
23
26
 
24
- <button type="submit"
25
- class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
26
- {{ t('mfa_challenge.submit') }}
27
- </button>
27
+ <button type="submit"
28
+ class="mt-6 w-full rounded-lg bg-gray-900 py-2.5 text-sm font-semibold text-white transition hover:opacity-90">
29
+ {{ t('mfa_challenge.submit') }}
30
+ </button>
31
+ @end
28
32
  </form>
29
33
 
30
- @if(passkeyAvailable)
34
+ @if(passkeyAvailable && !noEnrollment)
31
35
  {{-- Passkey como alternativa ao código TOTP. O form é submetido por página
32
36
  inteira (não fetch) para que o 303 de volta ao client navegue o browser. --}}
33
37
  <form id="passkey-form" method="POST" action="/auth/interaction/{{ uid }}/passkey/verify" class="mt-4">
@@ -41,18 +45,20 @@
41
45
  <p id="passkey-error" class="mt-3 hidden text-sm text-red-600">{{ t('mfa_challenge.passkey_error') }}</p>
42
46
  @end
43
47
 
44
- <details class="mt-6 text-sm text-gray-600">
45
- <summary class="cursor-pointer hover:underline">{{ t('mfa_challenge.recovery_summary') }}</summary>
46
- <form method="POST" action="/auth/interaction/{{ uid }}/mfa" class="mt-3">
47
- <input type="hidden" name="_csrf" value="{{ csrfToken }}">
48
- <input name="recoveryCode" placeholder="xxxxx-xxxxx"
49
- class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
50
- <button type="submit"
51
- class="mt-3 w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
52
- {{ t('mfa_challenge.recovery_submit') }}
53
- </button>
54
- </form>
55
- </details>
48
+ @if(!noEnrollment)
49
+ <details class="mt-6 text-sm text-gray-600">
50
+ <summary class="cursor-pointer hover:underline">{{ t('mfa_challenge.recovery_summary') }}</summary>
51
+ <form method="POST" action="/auth/interaction/{{ uid }}/mfa" class="mt-3">
52
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
53
+ <input name="recoveryCode" placeholder="xxxxx-xxxxx"
54
+ class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900" />
55
+ <button type="submit"
56
+ class="mt-3 w-full rounded-lg border border-gray-300 py-2.5 text-sm font-semibold text-gray-700 transition hover:bg-gray-50">
57
+ {{ t('mfa_challenge.recovery_submit') }}
58
+ </button>
59
+ </form>
60
+ </details>
61
+ @end
56
62
  </div>
57
63
 
58
64
  @if(passkeyAvailable)
package/build/index.d.ts CHANGED
@@ -10,8 +10,8 @@ export { resolveAdmin, resolveWebauthn, resolveDynamicRegistration } from './src
10
10
  export type { WebauthnConfigInput, ResolvedWebauthnConfig } from './src/define_config.js';
11
11
  export { lucidAccountStore } from './src/accounts/lucid_account_store.js';
12
12
  export type { LucidAccountStoreOptions, AccountSecretEncrypter, } from './src/accounts/lucid_account_store.js';
13
- export type { AccountStore, CoreAccountStore, AdminCapability, MfaCapability, WebauthnCapability, ProviderIdentityCapability, AuthAccount, CreateAccountInput, LinkProviderIdentityInput, ListAccountsParams, Paginated, PasskeySummary, } from './src/accounts/account_store.js';
14
- export { supportsMfa, supportsPasskeys, supportsProviderIdentity, } from './src/accounts/account_store.js';
13
+ export type { AccountStore, CoreAccountStore, AdminCapability, MfaCapability, WebauthnCapability, ProviderIdentityCapability, AccountSecurityCapability, AuthAccount, CreateAccountInput, LinkProviderIdentityInput, ListAccountsParams, Paginated, PasskeySummary, } from './src/accounts/account_store.js';
14
+ export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, } from './src/accounts/account_store.js';
15
15
  export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
16
16
  export type { ProviderIdentityRow, ProviderIdentityClass, } from './src/mixins/with_provider_identity.js';
17
17
  export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
@@ -26,12 +26,13 @@ export { inertiaRenderer } from './src/host/renderers/inertia_renderer.js';
26
26
  export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
27
27
  export { brandFor, isFirstParty } from './src/host/branding.js';
28
28
  export type { BrandingConfig, ClientBrand } from './src/host/branding.js';
29
- export { resolveMessages, translate, DEFAULT_MESSAGES, DEFAULT_LOCALE } from './src/host/i18n.js';
29
+ export { resolveMessages, translate, DEFAULT_MESSAGES, PT_BR_MESSAGES, BUILTIN_MESSAGES, DEFAULT_LOCALE, } from './src/host/i18n.js';
30
30
  export type { I18nConfig, AuthMessages } from './src/host/i18n.js';
31
31
  export type { AuthHostRenderer, AuthSocialConfig } from './src/define_config.js';
32
32
  export { registerAuthHost } from './src/host/register_auth_host.js';
33
33
  export type { AuthHostOptions } from './src/host/register_auth_host.js';
34
- export { resolveRateLimit } from './src/define_config.js';
34
+ export { resolveRateLimit, resolveNotifications } from './src/define_config.js';
35
+ export type { NotificationsConfigInput, ResolvedNotificationsConfig, } from './src/define_config.js';
35
36
  export type { RateLimitConfigInput, RateLimitBucket, ResolvedRateLimitConfig, } from './src/define_config.js';
36
37
  export { createAuthThrottles } from './src/host/rate_limit.js';
37
38
  export type { AuthThrottles, ThrottleMiddleware } from './src/host/rate_limit.js';
package/build/index.js CHANGED
@@ -7,7 +7,7 @@ export { OidcService } from './src/provider/oidc_service.js';
7
7
  export { registerOidcRoutes } from './src/register_routes.js';
8
8
  export { resolveAdmin, resolveWebauthn, resolveDynamicRegistration } from './src/define_config.js';
9
9
  export { lucidAccountStore } from './src/accounts/lucid_account_store.js';
10
- export { supportsMfa, supportsPasskeys, supportsProviderIdentity, } from './src/accounts/account_store.js';
10
+ export { supportsMfa, supportsPasskeys, supportsProviderIdentity, supportsAccountSecurity, } from './src/accounts/account_store.js';
11
11
  export { withProviderIdentity } from './src/mixins/with_provider_identity.js';
12
12
  export { withWebauthnCredential } from './src/mixins/with_webauthn_credential.js';
13
13
  export { lucidPatStore } from './src/pat/lucid_pat_store.js';
@@ -17,9 +17,9 @@ export { withAuditLog } from './src/mixins/with_audit_log.js';
17
17
  export { inertiaRenderer } from './src/host/renderers/inertia_renderer.js';
18
18
  export { edgeRenderer } from './src/host/renderers/edge_renderer.js';
19
19
  export { brandFor, isFirstParty } from './src/host/branding.js';
20
- export { resolveMessages, translate, DEFAULT_MESSAGES, DEFAULT_LOCALE } from './src/host/i18n.js';
20
+ export { resolveMessages, translate, DEFAULT_MESSAGES, PT_BR_MESSAGES, BUILTIN_MESSAGES, DEFAULT_LOCALE, } from './src/host/i18n.js';
21
21
  export { registerAuthHost } from './src/host/register_auth_host.js';
22
- export { resolveRateLimit } from './src/define_config.js';
22
+ export { resolveRateLimit, resolveNotifications } from './src/define_config.js';
23
23
  export { createAuthThrottles } from './src/host/rate_limit.js';
24
24
  /**
25
25
  * Configure hook + stubsRoot resolvidos pelo `node ace configure @dudousxd/adonis-authkit-server`.
@@ -83,6 +83,49 @@ export interface AdminCapability {
83
83
  /** Substitui as roles globais de uma conta. */
84
84
  setGlobalRoles(accountId: string, roles: string[]): Promise<void>;
85
85
  }
86
+ /**
87
+ * Self-service de segurança da conta (console de conta): trocar a senha e o
88
+ * e-mail (com confirmação no NOVO endereço). É uma CAPACIDADE opcional — stores
89
+ * sem suporte omitem os métodos e a UI esconde a seção correspondente.
90
+ *
91
+ * A troca de e-mail usa um token de confirmação que viaja para o NOVO endereço
92
+ * ({@link requestEmailChange}) e é consumido por {@link confirmEmailChange}. O
93
+ * store default (Lucid) reaproveita a coluna `emailVerificationToken` codificando
94
+ * um payload `ec:<email>:<token>` — assim NÃO exige migração nova (ver
95
+ * `lucid_store/core.ts`). O tradeoff é que um token de verificação de cadastro e
96
+ * um de troca de e-mail não coexistem (mesma coluna); na prática são fluxos
97
+ * distintos no tempo.
98
+ */
99
+ export interface AccountSecurityCapability {
100
+ /**
101
+ * Define uma nova senha para a conta (após o controller confirmar a senha ATUAL
102
+ * via {@link CoreAccountStore.verifyCredentials}). Retorna false se a conta não
103
+ * existe.
104
+ */
105
+ changePassword(accountId: string, newPassword: string): Promise<boolean>;
106
+ /**
107
+ * Inicia a troca de e-mail: gera um token de confirmação para o `newEmail` e o
108
+ * persiste. Retorna o token + a conta, ou null se a conta não existe OU se o
109
+ * `newEmail` já pertence a outra conta.
110
+ */
111
+ requestEmailChange(accountId: string, newEmail: string): Promise<{
112
+ token: string;
113
+ account: AuthAccount;
114
+ newEmail: string;
115
+ } | null>;
116
+ /**
117
+ * Confirma a troca de e-mail consumindo o token (single-use). Em caso de
118
+ * sucesso aplica o novo e-mail, marca-o como verificado e limpa o token.
119
+ * Retorna `{ ok: true, account, newEmail }` ou `{ ok: false }`.
120
+ */
121
+ confirmEmailChange(token: string): Promise<{
122
+ ok: true;
123
+ account: AuthAccount;
124
+ newEmail: string;
125
+ } | {
126
+ ok: false;
127
+ }>;
128
+ }
86
129
  /**
87
130
  * Account linking por identidade de provider (Google, GitHub, …).
88
131
  * `(provider, providerUserId)` é a chave estável vinda do provider OAuth — não
@@ -184,10 +227,12 @@ export interface WebauthnCapability {
184
227
  * presentes-mas-lançando). Use os type guards {@link supportsMfa},
185
228
  * {@link supportsPasskeys}, {@link supportsProviderIdentity} para estreitar.
186
229
  */
187
- export type AccountStore = CoreAccountStore & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability>;
230
+ export type AccountStore = CoreAccountStore & Partial<MfaCapability & WebauthnCapability & ProviderIdentityCapability & AccountSecurityCapability>;
188
231
  /** Type guard: o store implementa a capacidade de MFA / TOTP. */
189
232
  export declare function supportsMfa(store: AccountStore): store is AccountStore & MfaCapability;
190
233
  /** Type guard: o store implementa a capacidade de passkeys / WebAuthn. */
191
234
  export declare function supportsPasskeys(store: AccountStore): store is AccountStore & WebauthnCapability;
192
235
  /** Type guard: o store implementa account linking por identidade de provider. */
193
236
  export declare function supportsProviderIdentity(store: AccountStore): store is AccountStore & ProviderIdentityCapability;
237
+ /** Type guard: o store implementa o self-service de segurança (senha/e-mail). */
238
+ export declare function supportsAccountSecurity(store: AccountStore): store is AccountStore & AccountSecurityCapability;
@@ -10,3 +10,7 @@ export function supportsPasskeys(store) {
10
10
  export function supportsProviderIdentity(store) {
11
11
  return typeof store.findByProviderIdentity === 'function';
12
12
  }
13
+ /** Type guard: o store implementa o self-service de segurança (senha/e-mail). */
14
+ export function supportsAccountSecurity(store) {
15
+ return typeof store.changePassword === 'function';
16
+ }
@@ -1,8 +1,9 @@
1
- import type { CoreAccountStore } from '../account_store.js';
1
+ import type { AccountSecurityCapability, CoreAccountStore } from '../account_store.js';
2
2
  import type { LucidStoreContext } from './shared.js';
3
3
  /**
4
4
  * Núcleo SEMPRE presente do {@link CoreAccountStore} sobre um model Lucid:
5
- * identidade, cadastro, reset de senha, verificação de e-mail e administração
6
- * (listagem paginada + roles globais).
5
+ * identidade, cadastro, reset de senha, verificação de e-mail, administração
6
+ * (listagem paginada + roles globais) e o self-service de segurança
7
+ * ({@link AccountSecurityCapability}: trocar senha/e-mail).
7
8
  */
8
- export declare function buildCore(ctx: LucidStoreContext): CoreAccountStore;
9
+ export declare function buildCore(ctx: LucidStoreContext): CoreAccountStore & AccountSecurityCapability;