@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.
- package/build/commands/commands.json +28 -0
- package/build/commands/doctor.d.ts +10 -0
- package/build/commands/doctor.js +66 -0
- package/build/commands/rotate_keys.d.ts +10 -0
- package/build/commands/rotate_keys.js +53 -0
- package/build/host/views/account/email-confirmed.edge +15 -0
- package/build/host/views/account/security.edge +83 -0
- package/build/host/views/account/tokens.edge +7 -4
- package/build/host/views/admin/sessions.edge +89 -0
- package/build/host/views/admin/users.edge +1 -0
- package/build/host/views/mfa-challenge.edge +29 -23
- package/build/index.d.ts +5 -4
- package/build/index.js +3 -3
- package/build/src/accounts/account_store.d.ts +46 -1
- package/build/src/accounts/account_store.js +4 -0
- package/build/src/accounts/lucid_store/core.d.ts +5 -4
- package/build/src/accounts/lucid_store/core.js +67 -2
- package/build/src/adapters/adapter_contract.d.ts +17 -0
- package/build/src/adapters/database_adapter.d.ts +9 -5
- package/build/src/adapters/database_adapter.js +13 -6
- package/build/src/adapters/redis_adapter.d.ts +11 -5
- package/build/src/adapters/redis_adapter.js +16 -7
- package/build/src/audit/audit_sink.d.ts +1 -1
- package/build/src/define_config.d.ts +102 -0
- package/build/src/define_config.js +46 -3
- package/build/src/doctor/checks.d.ts +51 -0
- package/build/src/doctor/checks.js +231 -0
- package/build/src/host/admin_clients_service.js +12 -5
- package/build/src/host/admin_sessions_service.d.ts +63 -0
- package/build/src/host/admin_sessions_service.js +127 -0
- package/build/src/host/controllers/account_mfa_controller.js +6 -2
- package/build/src/host/controllers/account_security_controller.d.ts +16 -0
- package/build/src/host/controllers/account_security_controller.js +119 -0
- package/build/src/host/controllers/account_session_controller.js +2 -1
- package/build/src/host/controllers/admin/admin_sessions_controller.d.ts +14 -0
- package/build/src/host/controllers/admin/admin_sessions_controller.js +64 -0
- package/build/src/host/controllers/interaction_controller.d.ts +11 -0
- package/build/src/host/controllers/interaction_controller.js +55 -12
- package/build/src/host/default_mailer.d.ts +17 -0
- package/build/src/host/default_mailer.js +94 -9
- package/build/src/host/email_templates.d.ts +4 -0
- package/build/src/host/email_templates.js +5 -2
- package/build/src/host/i18n.d.ts +358 -11
- package/build/src/host/i18n.js +393 -12
- package/build/src/host/login_notify.d.ts +20 -0
- package/build/src/host/login_notify.js +71 -0
- package/build/src/host/register_auth_host.js +12 -0
- package/build/src/host/validators.d.ts +32 -0
- package/build/src/host/validators.js +14 -0
- package/build/src/keys/keystore.d.ts +43 -0
- package/build/src/keys/keystore.js +74 -0
- package/build/src/observability/metrics_controller.js +4 -4
- package/build/src/provider/build_provider.js +23 -0
- package/build/src/provider/device_sources.d.ts +6 -0
- package/build/src/provider/device_sources.js +65 -0
- package/build/src/provider/interaction_actions.d.ts +6 -1
- package/build/src/provider/interaction_actions.js +9 -2
- 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
|
-
<
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
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">← {{ 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class="
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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;
|