@dudousxd/adonis-authkit-server 0.2.0 → 0.4.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/client_form.edge +83 -0
- package/build/host/views/admin/clients.edge +68 -3
- 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 +4 -3
- package/build/index.js +2 -2
- 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 +29 -0
- package/build/src/adapters/database_adapter.d.ts +12 -1
- package/build/src/adapters/database_adapter.js +24 -0
- package/build/src/adapters/redis_adapter.d.ts +14 -1
- package/build/src/adapters/redis_adapter.js +35 -0
- 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.d.ts +65 -0
- package/build/src/host/admin_clients_service.js +143 -0
- 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_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_clients_controller.d.ts +17 -3
- package/build/src/host/controllers/admin/admin_clients_controller.js +158 -4
- 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 +49 -10
- package/build/src/host/default_mailer.d.ts +17 -0
- package/build/src/host/default_mailer.js +51 -0
- package/build/src/host/i18n.d.ts +80 -0
- package/build/src/host/i18n.js +86 -1
- 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 +20 -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/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/build/src/provider/oidc_service.d.ts +15 -0
- package/build/src/provider/oidc_service.js +27 -0
- 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,83 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="pt-br"><head><meta charset="utf-8"><title>{{ mode === 'edit' ? t('admin.clients.edit_title') : t('admin.clients.new_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">{{ mode === 'edit' ? t('admin.clients.edit_title') : t('admin.clients.new_title') }}</h1>
|
|
10
|
+
</div>
|
|
11
|
+
<a href="/admin/clients" class="text-sm text-gray-500 hover:underline">{{ t('admin.clients.back') }}</a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<form
|
|
15
|
+
method="POST"
|
|
16
|
+
action="{{ mode === 'edit' ? `/admin/clients/${client.clientId}/edit` : '/admin/clients' }}"
|
|
17
|
+
class="space-y-5 rounded-xl bg-white p-6 shadow-sm ring-1 ring-black/5"
|
|
18
|
+
>
|
|
19
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
20
|
+
|
|
21
|
+
<div>
|
|
22
|
+
<label class="block text-sm font-medium text-gray-700">{{ t('admin.clients.field_client_id') }}</label>
|
|
23
|
+
@if(mode === 'edit')
|
|
24
|
+
<input type="text" value="{{ client.clientId }}" disabled
|
|
25
|
+
class="mt-1 w-full rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 font-mono text-sm text-gray-500" />
|
|
26
|
+
@else
|
|
27
|
+
<input type="text" name="client_id" value="{{ client.clientId }}" placeholder="{{ t('admin.clients.field_client_id_placeholder') }}"
|
|
28
|
+
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 font-mono text-sm outline-none focus:border-gray-900" />
|
|
29
|
+
<p class="mt-1 text-xs text-gray-400">{{ t('admin.clients.field_client_id_help') }}</p>
|
|
30
|
+
@end
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div>
|
|
34
|
+
<label class="block text-sm font-medium text-gray-700">{{ t('admin.clients.field_redirect_uris') }}</label>
|
|
35
|
+
<textarea name="redirect_uris" rows="3" placeholder="https://app.exemplo.com/callback"
|
|
36
|
+
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 font-mono text-sm outline-none focus:border-gray-900">{{ client.redirectUris.join('\n') }}</textarea>
|
|
37
|
+
<p class="mt-1 text-xs text-gray-400">{{ t('admin.clients.field_redirect_uris_help') }}</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div>
|
|
41
|
+
<label class="block text-sm font-medium text-gray-700">{{ t('admin.clients.field_post_logout_uris') }}</label>
|
|
42
|
+
<textarea name="post_logout_redirect_uris" rows="2"
|
|
43
|
+
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 font-mono text-sm outline-none focus:border-gray-900">{{ client.postLogoutRedirectUris.join('\n') }}</textarea>
|
|
44
|
+
<p class="mt-1 text-xs text-gray-400">{{ t('admin.clients.field_post_logout_uris_help') }}</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div>
|
|
48
|
+
<span class="block text-sm font-medium text-gray-700">{{ t('admin.clients.field_grant_types') }}</span>
|
|
49
|
+
<div class="mt-2 space-y-1">
|
|
50
|
+
<label class="flex items-center gap-2 text-sm text-gray-700">
|
|
51
|
+
<input type="checkbox" name="grant_types" value="authorization_code" {{ client.grants.includes('authorization_code') ? 'checked' : '' }}>
|
|
52
|
+
authorization_code
|
|
53
|
+
</label>
|
|
54
|
+
<label class="flex items-center gap-2 text-sm text-gray-700">
|
|
55
|
+
<input type="checkbox" name="grant_types" value="refresh_token" {{ client.grants.includes('refresh_token') ? 'checked' : '' }}>
|
|
56
|
+
refresh_token
|
|
57
|
+
</label>
|
|
58
|
+
<label class="flex items-center gap-2 text-sm text-gray-700">
|
|
59
|
+
<input type="checkbox" name="grant_types" value="client_credentials" {{ client.grants.includes('client_credentials') ? 'checked' : '' }}>
|
|
60
|
+
client_credentials
|
|
61
|
+
</label>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div>
|
|
66
|
+
<label class="block text-sm font-medium text-gray-700">{{ t('admin.clients.field_auth_method') }}</label>
|
|
67
|
+
<select name="token_endpoint_auth_method"
|
|
68
|
+
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900">
|
|
69
|
+
<option value="client_secret_basic" {{ client.tokenEndpointAuthMethod === 'client_secret_basic' ? 'selected' : '' }}>client_secret_basic</option>
|
|
70
|
+
<option value="client_secret_post" {{ client.tokenEndpointAuthMethod === 'client_secret_post' ? 'selected' : '' }}>client_secret_post</option>
|
|
71
|
+
<option value="none" {{ client.tokenEndpointAuthMethod === 'none' ? 'selected' : '' }}>none ({{ t('admin.clients.public') }})</option>
|
|
72
|
+
</select>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="flex items-center justify-end gap-2 pt-2">
|
|
76
|
+
<a href="/admin/clients" class="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">{{ t('admin.clients.cancel') }}</a>
|
|
77
|
+
<button type="submit" class="rounded-lg bg-gray-900 px-4 py-2 text-sm font-semibold text-white hover:bg-gray-800">
|
|
78
|
+
{{ mode === 'edit' ? t('admin.clients.save') : t('admin.clients.create') }}
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
</form>
|
|
82
|
+
</div>
|
|
83
|
+
</body></html>
|
|
@@ -21,17 +21,30 @@
|
|
|
21
21
|
<a href="/admin/audit" class="text-gray-500 hover:underline">{{ t('admin.nav.audit') }}</a>
|
|
22
22
|
</nav>
|
|
23
23
|
|
|
24
|
+
@if(createdSecret)
|
|
25
|
+
<div class="mb-6 rounded-lg border border-emerald-300 bg-emerald-50 p-4 text-sm text-emerald-900">
|
|
26
|
+
<p class="font-semibold">{{ t('admin.clients.secret_once_title') }}</p>
|
|
27
|
+
<p class="mt-1">{{ t('admin.clients.secret_once_notice') }}</p>
|
|
28
|
+
<p class="mt-2 break-all font-mono text-xs">
|
|
29
|
+
<span class="text-emerald-700">client_id:</span> {{ createdSecret.clientId }}<br>
|
|
30
|
+
<span class="text-emerald-700">client_secret:</span> {{ createdSecret.clientSecret }}
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
@end
|
|
34
|
+
|
|
24
35
|
@if(dynamicEnabled)
|
|
25
36
|
<div class="mb-6 rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900">
|
|
26
37
|
{{ t('admin.clients.dynamic_notice') }}
|
|
27
38
|
</div>
|
|
28
39
|
@end
|
|
29
40
|
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
{{-- Clients estáticos (config, somente leitura). --}}
|
|
42
|
+
<h2 class="mb-2 text-sm font-semibold uppercase tracking-wide text-gray-500">{{ t('admin.clients.static_section') }}</h2>
|
|
43
|
+
<div class="mb-8 overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
|
|
44
|
+
@if(staticClients.length === 0)
|
|
32
45
|
<p class="p-6 text-sm text-gray-500">{{ t('admin.clients.empty') }}</p>
|
|
33
46
|
@else
|
|
34
|
-
@each(client in
|
|
47
|
+
@each(client in staticClients)
|
|
35
48
|
<div class="border-b border-gray-100 p-4 last:border-0">
|
|
36
49
|
<div class="flex items-center justify-between">
|
|
37
50
|
<p class="text-sm font-medium text-gray-900">{{ client.clientId }}</p>
|
|
@@ -47,5 +60,57 @@
|
|
|
47
60
|
@end
|
|
48
61
|
@end
|
|
49
62
|
</div>
|
|
63
|
+
|
|
64
|
+
{{-- Clients dinâmicos (adapter, com CRUD). --}}
|
|
65
|
+
<div class="mb-2 flex items-center justify-between">
|
|
66
|
+
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500">{{ t('admin.clients.dynamic_section') }}</h2>
|
|
67
|
+
@if(dynamicSupported)
|
|
68
|
+
<a href="/admin/clients/new" class="rounded-lg bg-gray-900 px-3 py-1.5 text-sm font-semibold text-white hover:bg-gray-800">
|
|
69
|
+
{{ t('admin.clients.new') }}
|
|
70
|
+
</a>
|
|
71
|
+
@end
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
@if(!dynamicSupported)
|
|
75
|
+
<div class="rounded-xl bg-white p-6 text-sm text-gray-500 shadow-sm ring-1 ring-black/5">
|
|
76
|
+
{{ t('admin.clients.dynamic_not_supported') }}
|
|
77
|
+
</div>
|
|
78
|
+
@else
|
|
79
|
+
<div class="overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-black/5">
|
|
80
|
+
@if(dynamicClients.length === 0)
|
|
81
|
+
<p class="p-6 text-sm text-gray-500">{{ t('admin.clients.dynamic_empty') }}</p>
|
|
82
|
+
@else
|
|
83
|
+
@each(client in dynamicClients)
|
|
84
|
+
<div class="border-b border-gray-100 p-4 last:border-0">
|
|
85
|
+
<div class="flex items-center justify-between gap-2">
|
|
86
|
+
<p class="break-all text-sm font-medium text-gray-900">{{ client.clientId }}</p>
|
|
87
|
+
<span class="shrink-0 rounded-full px-2 py-0.5 text-xs {{ client.confidential ? 'bg-gray-900 text-white' : 'bg-gray-100 text-gray-600' }}">
|
|
88
|
+
{{ client.confidential ? t('admin.clients.confidential') : t('admin.clients.public') }}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
<p class="mt-1 text-xs text-gray-500">{{ t('admin.clients.grants', { grants: client.grants.join(', ') }) }}</p>
|
|
92
|
+
@if(client.redirectUris.length > 0)
|
|
93
|
+
<p class="text-xs text-gray-400">{{ t('admin.clients.redirect_uris', { uris: client.redirectUris.join(', ') }) }}</p>
|
|
94
|
+
@end
|
|
95
|
+
<div class="mt-3 flex flex-wrap items-center gap-2">
|
|
96
|
+
<a href="/admin/clients/{{ client.clientId }}/edit" class="rounded border border-gray-300 px-3 py-1 text-xs hover:bg-gray-50">
|
|
97
|
+
{{ t('admin.clients.edit') }}
|
|
98
|
+
</a>
|
|
99
|
+
@if(client.confidential)
|
|
100
|
+
<form method="POST" action="/admin/clients/{{ client.clientId }}/regenerate-secret" onsubmit="return confirm('{{ t('admin.clients.regenerate_confirm') }}')">
|
|
101
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
102
|
+
<button type="submit" class="rounded border border-gray-300 px-3 py-1 text-xs hover:bg-gray-50">{{ t('admin.clients.regenerate_secret') }}</button>
|
|
103
|
+
</form>
|
|
104
|
+
@end
|
|
105
|
+
<form method="POST" action="/admin/clients/{{ client.clientId }}/delete" onsubmit="return confirm('{{ t('admin.clients.delete_confirm') }}')">
|
|
106
|
+
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
|
107
|
+
<button type="submit" class="rounded border border-red-300 px-3 py-1 text-xs text-red-700 hover:bg-red-50">{{ t('admin.clients.delete') }}</button>
|
|
108
|
+
</form>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
@end
|
|
112
|
+
@end
|
|
113
|
+
</div>
|
|
114
|
+
@end
|
|
50
115
|
</div>
|
|
51
116
|
</body></html>
|
|
@@ -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 }}">
|