feat(atm-escolinha): tela de login Athletic Map
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# GERADO por scripts/tema_login_escolinha.py — nao edite na mao.
|
||||
# Fonte: keycloak-theme/escolinha/{login,login-update-password,estilo,template}.ftl
|
||||
# Fonte: keycloak-theme/escolinha/*.ftl (login.ftl, login-update-password.ftl, estilo.ftl, template.ftl, error.ftl, info.ftl, logout-confirm.ftl, login-page-expired.ftl, login-reset-password.ftl, login-config-totp.ftl, login-otp.ftl, webauthn-register.ftl, webauthn-authenticate.ftl, select-authenticator.ftl)
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
@@ -487,7 +487,9 @@ data:
|
||||
.botao{
|
||||
display:flex;align-items:center;justify-content:center;gap:10px;
|
||||
width:100%;height:56px;border:0;border-radius:12px;
|
||||
font-size:16px;font-weight:700;cursor:pointer;
|
||||
/* `font-family:inherit`: <button> nao herda a fonte da pagina, e o
|
||||
"Tente outra forma" saia em Arial (2026-10-08). */
|
||||
font-family:inherit;font-size:16px;font-weight:700;cursor:pointer;
|
||||
transition:background .15s ease, border-color .15s ease;
|
||||
}
|
||||
.botao:focus-visible{outline:3px solid rgba(245,130,32,.45);outline-offset:2px}
|
||||
@@ -539,6 +541,81 @@ data:
|
||||
.aviso-success{background:#EAF6EC;color:#2F7D43;border:1px solid #C6E6CD}
|
||||
.aviso-warning,.aviso-info{background:#FFF5E8;color:#9A5A12;border:1px solid #F6DCB4}
|
||||
|
||||
/* ---------- Paginas escritas para o template.ftl (2026-10-08) ----------
|
||||
error, info, logout-confirm, esqueci a senha, pagina expirada, 2FA
|
||||
(configurar e digitar), passkey (cadastrar e entrar) e escolher metodo.
|
||||
Elas desenham com as classes do login (.campo, .caixa, .botao), e o que
|
||||
segue e so o que o login nao tinha. */
|
||||
.selo{
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
width:52px;height:52px;border-radius:50%;margin:0 0 18px;
|
||||
background:#FFF1E3;color:var(--orange);
|
||||
}
|
||||
.selo svg{width:26px;height:26px}
|
||||
.selo.erro{background:#FDECE9;color:#B23B28}
|
||||
.conteudo .lembrar{display:flex;margin:0;font-size:14px;font-weight:400;color:var(--muted)}
|
||||
.selo.ok{background:#EAF6EC;color:#2F7D43}
|
||||
.conteudo .sub{margin:0 0 14px}
|
||||
.conteudo form{margin-top:10px}
|
||||
.conteudo a.botao{color:#fff;text-decoration:none}
|
||||
.conteudo a.botao:hover{color:#fff}
|
||||
.conteudo a.botao.secundario,.conteudo a.botao.secundario:hover{color:var(--muted)}
|
||||
.conteudo a.botao.secundario:hover{color:var(--navy)}
|
||||
.conteudo .caixa input{padding:0 50px;height:54px;border-radius:12px;border-width:1.5px}
|
||||
.caixa input.codigo{letter-spacing:.32em;font-size:20px;font-weight:700;padding-left:50px}
|
||||
.erro-campo{display:block;margin-top:2px;font-size:13px;color:#B23B28}
|
||||
.voltar{display:block;text-align:center;margin-top:18px;font-size:14px;color:var(--blue)!important;text-decoration:none!important}
|
||||
.voltar:hover{text-decoration:underline!important}
|
||||
.detalhe{font-size:13px;color:var(--muted)}
|
||||
.detalhe code{font-family:ui-monospace,Consolas,monospace;font-size:12.5px;color:var(--navy);
|
||||
background:#fff;border:1px solid var(--line);border-radius:6px;padding:1px 6px;word-break:break-all}
|
||||
.conteudo .lista-acoes{margin:0 0 18px;padding:0;list-style:none}
|
||||
.lista-acoes li{display:flex;align-items:center;gap:10px;margin:0 0 8px;padding:11px 14px;
|
||||
background:#fff;border:1px solid var(--line);border-radius:11px;font-weight:600}
|
||||
.lista-acoes li::before{content:'';flex:0 0 8px;height:8px;border-radius:50%;background:var(--orange)}
|
||||
|
||||
/* Configurar o 2FA: os tres passos numerados, o QR e a chave manual. */
|
||||
.conteudo .passos{counter-reset:passo;margin:0 0 6px;padding:0;list-style:none}
|
||||
.passos>li{counter-increment:passo;position:relative;padding:0 0 16px 40px;margin:0}
|
||||
.passos>li::before{
|
||||
content:counter(passo);position:absolute;left:0;top:-2px;
|
||||
width:26px;height:26px;border-radius:50%;background:var(--navy);color:#fff;
|
||||
font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;
|
||||
}
|
||||
.passos p{margin:0 0 8px}
|
||||
.conteudo .apps{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}
|
||||
.apps li{margin:0;padding:4px 10px;font-size:13px;background:#fff;border:1px solid var(--line);border-radius:999px}
|
||||
.qr{display:inline-block;padding:10px;background:#fff;border:1px solid var(--line);border-radius:14px;line-height:0}
|
||||
.qr img{width:176px;height:176px;image-rendering:pixelated}
|
||||
.chave-manual{display:block;padding:12px 14px;background:#fff;border:1px dashed #CFCDC8;border-radius:11px;
|
||||
font-family:ui-monospace,Consolas,monospace;font-size:15px;font-weight:600;letter-spacing:.06em;word-break:break-all}
|
||||
.conteudo .config-manual{margin:8px 0 0;padding:0;list-style:none;font-size:13.5px;color:var(--muted)}
|
||||
.config-manual li{margin:0 0 2px}
|
||||
.passos .troca{font-size:14px}
|
||||
|
||||
/* Escolher o metodo (select-authenticator), as credenciais OTP e as passkeys:
|
||||
cartoes brancos clicaveis. `.conteudo .cartao` vence o `.conteudo button`
|
||||
laranja de cima, que e o botao de acao. */
|
||||
.cartoes{display:flex;flex-direction:column;gap:10px;margin:0}
|
||||
.conteudo .cartao{
|
||||
display:flex;align-items:center;gap:14px;width:100%;margin:0;padding:14px 16px;
|
||||
background:#fff;color:var(--navy);border:1.5px solid var(--line);border-radius:12px;
|
||||
font:inherit;font-size:15px;font-weight:400;text-align:left;cursor:pointer;
|
||||
transition:border-color .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
.conteudo button.cartao:hover,.conteudo label.cartao:hover{background:#fff;border-color:var(--orange)}
|
||||
.conteudo .cartao:focus-visible{outline:3px solid rgba(245,130,32,.45);outline-offset:2px}
|
||||
.cartao .ico{flex:0 0 40px;height:40px;border-radius:10px;background:#FFF1E3;color:var(--orange);
|
||||
display:flex;align-items:center;justify-content:center}
|
||||
.cartao .ico svg{width:22px;height:22px}
|
||||
.cartao .txt{flex:1;min-width:0}
|
||||
.cartao .txt b{display:block;font-size:15px;font-weight:700}
|
||||
.cartao .txt span{display:block;font-size:13px;color:var(--muted);margin-top:2px}
|
||||
.cartao .seta{color:#B6B4AF;display:flex}
|
||||
.credenciais input{position:absolute;opacity:0;pointer-events:none}
|
||||
.credenciais input:checked+label.cartao{border-color:var(--orange);box-shadow:0 0 0 3px rgba(245,130,32,.16)}
|
||||
.conteudo .cartao.estatico{cursor:default}
|
||||
|
||||
/* ---------- Telas estreitas ---------- */
|
||||
@media (max-width:980px){
|
||||
.stage{grid-template-columns:1fr}
|
||||
@@ -611,6 +688,15 @@ data:
|
||||
info o rodape informativo, so quando displayInfo
|
||||
socialProviders os provedores externos, quando houver
|
||||
|
||||
E DUAS SECOES QUE SAO DESTE TEMA (2026-10-08), usadas so pelas paginas
|
||||
escritas aqui (error, info, logout-confirm, login-otp...):
|
||||
|
||||
selo o icone redondo acima do titulo
|
||||
sub a linha cinza abaixo do titulo, como a do login
|
||||
|
||||
Uma pagina do tema BASE nunca as preenche (as dela sao if/elseif sobre os
|
||||
quatro nomes acima), entao para ela as duas saem vazias e nao desenham nada.
|
||||
|
||||
── O QUE ELA DELIBERADAMENTE NAO FAZ ────────────────────────────────────────
|
||||
|
||||
Nao carrega `properties.styles` nem `properties.scripts` do tema base: o
|
||||
@@ -625,6 +711,14 @@ data:
|
||||
Mesmo include do `login.ftl`: elas pesam ~400 KB em data URI e existem uma
|
||||
vez no ConfigMap.
|
||||
-->
|
||||
<#--
|
||||
As chaves pt-BR de link do Keycloak vem com seta de texto: "« Voltar para o
|
||||
aplicativo", "» Clique aqui para continuar". Num botao a seta sobra. Escrita
|
||||
com escape (\xAB, \xBB) para nao depender da codificacao de quem le o arquivo.
|
||||
-->
|
||||
<#function semSeta texto>
|
||||
<#return texto?replace("\xAB", "")?replace("\xBB", "")?trim>
|
||||
</#function>
|
||||
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false>
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -662,8 +756,20 @@ data:
|
||||
<div class="form">
|
||||
<span class="marca" role="img" aria-label="Athletic Map"></span>
|
||||
|
||||
<#assign selo><#nested "selo"></#assign>
|
||||
<#if selo?markup_string?trim?has_content>
|
||||
<#-- A pagina traz o proprio `<span class="selo [erro|ok]">`: a cor
|
||||
depende do que ela diz. -->
|
||||
${selo}
|
||||
</#if>
|
||||
|
||||
<h1><#nested "header"></h1>
|
||||
|
||||
<#assign sub><#nested "sub"></#assign>
|
||||
<#if sub?markup_string?trim?has_content>
|
||||
<p class="sub">${sub}</p>
|
||||
</#if>
|
||||
|
||||
<#-- A mesma regra do tema base: uma acao iniciada pelo proprio app nao
|
||||
mostra o aviso de "voce precisa completar isto", porque quem a
|
||||
iniciou foi o usuario, agora. -->
|
||||
@@ -681,7 +787,9 @@ data:
|
||||
<#if auth?has_content && auth.showTryAnotherWayLink()>
|
||||
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post">
|
||||
<input type="hidden" name="tryAnotherWay" value="on"/>
|
||||
<button class="entrar secundario" type="submit">${msg("doTryAnotherWay")}</button>
|
||||
<#-- `botao`: sem ele o "Tente outra forma" saia sem altura nem
|
||||
alinhamento — so a borda (achado em 2026-10-08). -->
|
||||
<button class="botao entrar secundario" type="submit">${msg("doTryAnotherWay")}</button>
|
||||
</form>
|
||||
</#if>
|
||||
|
||||
@@ -702,3 +810,631 @@ data:
|
||||
</html>
|
||||
</#macro>
|
||||
|
||||
error.ftl: |-
|
||||
<#--
|
||||
A tela de ERRO do Keycloak ("Sentimos muito..."): redirect_uri invalido,
|
||||
client inexistente, link de acao invalido ou ja usado, sessao perdida.
|
||||
|
||||
Ate 2026-10-08 era a `error.ftl` do tema BASE dentro do `template.ftl` deste:
|
||||
ja tinha a marca, mas o titulo "Sentimos muito..." e o texto tecnico solto
|
||||
("Parametro invalido: redirect_uri") sem dizer o que a pessoa faz agora. Quem
|
||||
chega aqui quase sempre so precisa voltar e tentar de novo.
|
||||
|
||||
Textos: as chaves pt-BR do Keycloak quando existem (`errorTitle`,
|
||||
`backToApplication`). O `traceIdSupportMessage` NAO tem traducao pt-BR no
|
||||
26.7.5 — sairia em ingles —, entao a frase do codigo de suporte e fixa aqui.
|
||||
|
||||
O id `kc-error-message` e o mesmo do base (o validador procura por ele).
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout displayMessage=false; section>
|
||||
<#if section = "selo">
|
||||
<span class="selo erro"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5"/><path d="M12 16.5h.01"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
Algo deu <span class="hl">errado</span>
|
||||
<#elseif section = "sub">
|
||||
${kcSanitize(msg("errorTitle"))?no_esc} Não foi possível concluir o seu pedido.
|
||||
<#elseif section = "form">
|
||||
<div id="kc-error-message">
|
||||
<#if message?? && message.summary?has_content>
|
||||
<div class="aviso aviso-error">${kcSanitize(message.summary)?no_esc}</div>
|
||||
</#if>
|
||||
<p>Volte para o aplicativo e tente de novo. Se continuar acontecendo, fale com o suporte.</p>
|
||||
<#if traceId??>
|
||||
<p id="traceId" class="detalhe">Se falar com o suporte, informe este código: <code>${traceId}</code></p>
|
||||
</#if>
|
||||
<#if !skipLink??>
|
||||
<#if client?? && client.baseUrl?has_content>
|
||||
<a id="backToApplication" class="botao entrar" href="${client.baseUrl}">${layout.semSeta(msg("backToApplication"))}</a>
|
||||
</#if>
|
||||
</#if>
|
||||
</div>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
info.ftl: |-
|
||||
<#--
|
||||
As telas de INFORMACAO do Keycloak: "execute estas acoes" (o link do e-mail de
|
||||
convite/troca de senha), "sua conta foi atualizada", "voce saiu da sessao",
|
||||
e-mail verificado, acao expirada com sessao, etc.
|
||||
|
||||
Ate 2026-10-08 era a `info.ftl` do tema BASE na moldura deste. Dois defeitos
|
||||
que motivaram esta: o titulo "Account updated" saia em INGLES (a chave
|
||||
`accountUpdatedTitle` nao tem traducao pt-BR no 26.7.5), e o titulo repetia
|
||||
palavra por palavra a frase de baixo.
|
||||
|
||||
TITULO: `messageHeader` quando o Keycloak manda um (traduzido aqui quando o
|
||||
pt-BR de fabrica nao tem); senao, a propria mensagem — e entao ela NAO se
|
||||
repete embaixo.
|
||||
|
||||
Os ids `kc-info-message` e os links sao os do base; o validador procura o id.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<#-- Chaves de titulo que o pt-BR do Keycloak 26.7.5 nao traduz. -->
|
||||
<#assign titulosPtBr = {"accountUpdatedTitle": "Conta atualizada"}>
|
||||
<#-- Nomes de acao obrigatoria que em pt-BR de fabrica sao tecnicos demais. -->
|
||||
<#assign acoesPtBr = {
|
||||
"webauthn-register-passwordless": "Cadastrar uma chave de seguran\xE7a (passkey)",
|
||||
"webauthn-register": "Cadastrar uma chave de seguran\xE7a para o segundo fator"}>
|
||||
<#assign sucesso = ((message.type)!'') == 'success' || (messageHeader!'') == 'accountUpdatedTitle'>
|
||||
<@layout.registrationLayout displayMessage=false; section>
|
||||
<#if section = "selo">
|
||||
<#if requiredActions??>
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="4" width="14" height="17" rx="2"/><path d="M9 4.5V3h6v1.5"/><path d="m9 12 2 2 4-4"/></svg></span>
|
||||
<#elseif sucesso>
|
||||
<span class="selo ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m8 12.5 2.7 2.7L16 10"/></svg></span>
|
||||
<#else>
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 7.5h.01"/></svg></span>
|
||||
</#if>
|
||||
<#elseif section = "header">
|
||||
<#if messageHeader??>
|
||||
${kcSanitize(titulosPtBr[messageHeader]!msg("${messageHeader}"))?no_esc}
|
||||
<#elseif requiredActions??>
|
||||
Antes de <span class="hl">continuar</span>
|
||||
<#else>
|
||||
${(message.summary)!''}
|
||||
</#if>
|
||||
<#elseif section = "sub">
|
||||
<#if messageHeader?? || requiredActions??>${(message.summary)!''}</#if>
|
||||
<#elseif section = "form">
|
||||
<div id="kc-info-message">
|
||||
<#if requiredActions??>
|
||||
<ul class="lista-acoes">
|
||||
<#list requiredActions as acao>
|
||||
<li>${kcSanitize(acoesPtBr[acao]!msg("requiredAction.${acao}"))?no_esc}</li>
|
||||
</#list>
|
||||
</ul>
|
||||
</#if>
|
||||
<#if !skipLink??>
|
||||
<#if pageRedirectUri?has_content>
|
||||
<a class="botao entrar" href="${pageRedirectUri}">${layout.semSeta(msg("backToApplication"))}</a>
|
||||
<#elseif actionUri?has_content>
|
||||
<a class="botao entrar" href="${actionUri}">${msg("doContinue")}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg></a>
|
||||
<#elseif (client.baseUrl)?has_content>
|
||||
<a class="botao entrar" href="${client.baseUrl}">${layout.semSeta(msg("backToApplication"))}</a>
|
||||
</#if>
|
||||
</#if>
|
||||
</div>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
logout-confirm.ftl: |-
|
||||
<#--
|
||||
CONFIRMAR A SAIDA — o que o Keycloak mostra quando o logout chega sem
|
||||
`id_token_hint` (link antigo, app que perdeu o token, alguem digitando a URL).
|
||||
|
||||
O "Sair" do Painel do dono ja manda o id_token e nao passa por aqui
|
||||
(2026-10-07); esta tela e para todo o resto. Ate 2026-10-08 era a do tema
|
||||
BASE na moldura deste: "Saindo" / "Voce realmente deseja sair?" e o botao.
|
||||
|
||||
O botao precisa de `name="confirmLogout"` e o form do `session_code`: e o que
|
||||
o Keycloak le no POST. O base desabilitava o botao no onsubmit, e um botao
|
||||
desabilitado nao vai no POST — aqui nao se desabilita nada.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout; section>
|
||||
<#if section = "selo">
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3"/><path d="M10 17l-5-5 5-5"/><path d="M5 12h11"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
Sair da <span class="hl">conta</span>
|
||||
<#elseif section = "sub">
|
||||
${msg("logoutConfirmHeader")}
|
||||
<#elseif section = "form">
|
||||
<div id="kc-logout-confirm">
|
||||
<form class="form-actions" action="${url.logoutConfirmAction}" method="POST">
|
||||
<input type="hidden" name="session_code" value="${logoutConfirm.code}">
|
||||
<button class="botao entrar" name="confirmLogout" id="kc-logout" type="submit" value="${msg("doLogout")}">
|
||||
${msg("doLogout")}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
<div id="kc-info-message">
|
||||
<#if !logoutConfirm.skipLink>
|
||||
<#if (client.baseUrl)?has_content>
|
||||
<a class="voltar" href="${client.baseUrl}">${layout.semSeta(msg("backToApplication"))}</a>
|
||||
</#if>
|
||||
</#if>
|
||||
</div>
|
||||
</div>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
login-page-expired.ftl: |-
|
||||
<#--
|
||||
PAGINA EXPIRADA — o Keycloak mostra quando chega um formulario de uma etapa
|
||||
que o login ja passou (voltar do navegador e reenviar, aba antiga).
|
||||
|
||||
O base dizia "Para reiniciar o processo de login [Clique aqui] . Para
|
||||
continuar o processo de login [Clique aqui] ." — dois links soltos com ponto
|
||||
depois. Aqui sao dois botoes: o principal recomeca (o que quase todo mundo
|
||||
quer), o secundario continua de onde parou.
|
||||
|
||||
Os ids `loginRestartLink` e `loginContinueLink` sao os do base.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout; section>
|
||||
<#if section = "selo">
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2.5 2"/><path d="M9.5 2.5h5"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
A página <span class="hl">expirou</span>
|
||||
<#elseif section = "sub">
|
||||
Esta etapa do login não vale mais. Escolha como seguir:
|
||||
<#elseif section = "form">
|
||||
<a id="loginRestartLink" class="botao entrar" href="${url.loginRestartFlowUrl}">Recomeçar o login</a>
|
||||
<a id="loginContinueLink" class="botao entrar secundario" href="${url.loginAction}">Continuar de onde parei</a>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
login-reset-password.ftl: |-
|
||||
<#--
|
||||
ESQUECEU SUA SENHA — o link "Esqueceu sua senha?" do login cai aqui.
|
||||
|
||||
Ate 2026-10-08 era a pagina do tema BASE na moldura deste: campo sem o icone e
|
||||
a altura do login, "« Voltar ao Login" ACIMA do botao e a instrucao no rodape.
|
||||
Aqui a instrucao vira o subtitulo, o campo e o mesmo do login e o "voltar"
|
||||
fica embaixo, onde se procura.
|
||||
|
||||
O Keycloak le `username` no POST. O id do form (`kc-reset-password-form`) e o
|
||||
do base; o validador procura por ele. Depois de enviar, o Keycloak volta ao
|
||||
LOGIN com o aviso "Voce devera receber um e-mail..." (mesmo para e-mail que
|
||||
nao existe, para nao revelar quem tem conta) — essa tela e a login.ftl.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username'); section>
|
||||
<#if section = "selo">
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="11" rx="2.4"/><path d="M8 10V7a4 4 0 0 1 7.6-1.7"/><path d="M12 14.5v2.5"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
Esqueceu sua <span class="hl">senha?</span>
|
||||
<#elseif section = "sub">
|
||||
<#if realm.duplicateEmailsAllowed>${msg("emailInstructionUsername")}<#else>${msg("emailInstruction")}</#if>
|
||||
<#elseif section = "form">
|
||||
<form id="kc-reset-password-form" action="${url.loginAction}" method="post" novalidate>
|
||||
<div class="campo">
|
||||
<label class="rotulo" for="username"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
|
||||
<div class="caixa">
|
||||
<span class="icone">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="m3.5 7 8.5 6 8.5-6"/></svg>
|
||||
</span>
|
||||
<input type="text" id="username" name="username" autofocus autocomplete="username"
|
||||
placeholder="seu@email.com" value="${(auth.attemptedUsername!'')}" dir="ltr"
|
||||
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
|
||||
class="<#if messagesPerField.existsError('username')>erro</#if>"/>
|
||||
</div>
|
||||
<#if messagesPerField.existsError('username')>
|
||||
<span id="input-error-username" class="erro-campo" aria-live="polite">${kcSanitize(messagesPerField.get('username'))?no_esc}</span>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<button class="botao entrar" type="submit">
|
||||
${msg("doSubmit")}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
<a class="voltar" href="${url.loginUrl}">${layout.semSeta(msg("backToLogin"))}</a>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
login-config-totp.ftl: |-
|
||||
<#--
|
||||
CONFIGURAR O 2FA (TOTP) — o dono (`atm_admin`) cai aqui no primeiro login
|
||||
depois do 2FA obrigatorio (fluxo `browser-2fa-dono`, 2026-10-07).
|
||||
|
||||
Ate 2026-10-08 era a pagina do tema BASE na moldura deste: funcionava, mas com
|
||||
o "*" de obrigatorio numa linha sozinha, o passo 3 mandando "clicar em Ok"
|
||||
num botao que se chama "Enviar", e o QR solto no texto.
|
||||
|
||||
NAO MUDE OS IDS: o robo do e2e (`frontend/e2e/apoio.ts`, passarPeloKeycloak)
|
||||
acha a tela por `kc-totp-settings-form`, troca para a chave em texto por
|
||||
`mode-manual`, le `kc-totp-secret-key` e preenche `totp`, `userLabel` e
|
||||
`saveTOTPBtn`. O Keycloak le no POST: totp, totpSecret, userLabel, mode,
|
||||
logout-sessions e cancel-aia.
|
||||
|
||||
`totp.policy.algorithmKey` e o getAlgorithmKey() do base, pela propriedade.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout displayRequiredFields=false displayMessage=!messagesPerField.existsError('totp','userLabel'); section>
|
||||
<#if section = "selo">
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 4.5 6v5.5c0 4.6 3.1 7.9 7.5 9.5 4.4-1.6 7.5-4.9 7.5-9.5V6L12 3Z"/><path d="m9 12 2 2 4-4"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
Verificação em <span class="hl">duas etapas</span>
|
||||
<#elseif section = "sub">
|
||||
Além da senha, cada login vai pedir um código do aplicativo autenticador do seu celular.
|
||||
<#elseif section = "form">
|
||||
<ol id="kc-totp-settings" class="passos">
|
||||
<li>
|
||||
<p>${msg("loginTotpStep1")}</p>
|
||||
<ul id="kc-totp-supported-apps" class="apps">
|
||||
<#list totp.supportedApplications as app>
|
||||
<li>${msg(app)}</li>
|
||||
</#list>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<#if mode?? && mode = "manual">
|
||||
<li>
|
||||
<p>${msg("loginTotpManualStep2")}</p>
|
||||
<span id="kc-totp-secret-key" class="chave-manual">${totp.totpSecretEncoded}</span>
|
||||
<ul class="config-manual">
|
||||
<li id="kc-totp-type">${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}</li>
|
||||
<li id="kc-totp-algorithm">${msg("loginTotpAlgorithm")}: ${totp.policy.algorithmKey}</li>
|
||||
<li id="kc-totp-digits">${msg("loginTotpDigits")}: ${totp.policy.digits}</li>
|
||||
<#if totp.policy.type = "totp">
|
||||
<li id="kc-totp-period">${msg("loginTotpInterval")}: ${totp.policy.period}</li>
|
||||
<#elseif totp.policy.type = "hotp">
|
||||
<li id="kc-totp-counter">${msg("loginTotpCounter")}: ${totp.policy.initialCounter}</li>
|
||||
</#if>
|
||||
</ul>
|
||||
<p style="margin-top:10px"><a href="${totp.qrUrl}" id="mode-barcode" class="troca">${msg("loginTotpScanBarcode")}</a></p>
|
||||
</li>
|
||||
<#else>
|
||||
<li>
|
||||
<p>${msg("loginTotpStep2")}</p>
|
||||
<span class="qr"><img id="kc-totp-secret-qr-code" src="data:image/png;base64, ${totp.totpSecretQrCode}" alt="Código QR para o aplicativo autenticador"></span>
|
||||
<p style="margin-top:10px"><a href="${totp.manualUrl}" id="mode-manual" class="troca">${msg("loginTotpUnableToScan")}</a></p>
|
||||
</li>
|
||||
</#if>
|
||||
|
||||
<li>
|
||||
<p>Digite o código que o aplicativo mostra e dê um nome a este celular.</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<form action="${url.loginAction}" id="kc-totp-settings-form" method="post" novalidate>
|
||||
<div class="campo">
|
||||
<label class="rotulo" for="totp">${msg("authenticatorCode")}</label>
|
||||
<div class="caixa">
|
||||
<span class="icone">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2.5" width="12" height="19" rx="2.5"/><path d="M11 18.5h2"/></svg>
|
||||
</span>
|
||||
<input type="text" id="totp" name="totp" autocomplete="one-time-code" inputmode="numeric"
|
||||
placeholder="000000" dir="ltr" class="codigo<#if messagesPerField.existsError('totp')> erro</#if>"
|
||||
aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>"/>
|
||||
</div>
|
||||
<#if messagesPerField.existsError('totp')>
|
||||
<span id="input-error-otp-code" class="erro-campo" aria-live="polite">${kcSanitize(messagesPerField.get('totp'))?no_esc}</span>
|
||||
</#if>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value="${totp.totpSecret}"/>
|
||||
<#if mode??><input type="hidden" id="mode" name="mode" value="${mode}"/></#if>
|
||||
</div>
|
||||
|
||||
<div class="campo">
|
||||
<label class="rotulo" for="userLabel">${msg("loginTotpDeviceName")}<#if !(totp.otpCredentials?size gte 1)> <span style="font-weight:400;color:var(--muted)">(opcional)</span></#if></label>
|
||||
<div class="caixa">
|
||||
<span class="icone">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h10l6 5-6 5H4z"/></svg>
|
||||
</span>
|
||||
<input type="text" id="userLabel" name="userLabel" autocomplete="off" placeholder="Ex.: meu celular" dir="ltr"
|
||||
class="<#if messagesPerField.existsError('userLabel')>erro</#if>"
|
||||
aria-invalid="<#if messagesPerField.existsError('userLabel')>true</#if>"/>
|
||||
</div>
|
||||
<#if messagesPerField.existsError('userLabel')>
|
||||
<span id="input-error-otp-label" class="erro-campo" aria-live="polite">${kcSanitize(messagesPerField.get('userLabel'))?no_esc}</span>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<label class="lembrar">
|
||||
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on"> ${msg("logoutOtherSessions")}
|
||||
</label>
|
||||
|
||||
<button type="submit" class="botao entrar" id="saveTOTPBtn">
|
||||
Ativar
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
|
||||
</button>
|
||||
<#if isAppInitiatedAction??>
|
||||
<button type="submit" class="botao entrar secundario" id="cancelTOTPBtn" name="cancel-aia" value="true">${msg("doCancel")}</button>
|
||||
</#if>
|
||||
</form>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
login-otp.ftl: |-
|
||||
<#--
|
||||
DIGITAR O CODIGO DO 2FA — o dono, a cada login com senha (o login por
|
||||
passkey dispensa; ver `browser-2fa-dono` no molde).
|
||||
|
||||
Ate 2026-10-08 era a pagina do tema BASE na moldura deste: titulo "Entrar",
|
||||
como se fosse o login de novo, e um campo sem dizer de onde vem o codigo.
|
||||
|
||||
NAO MUDE OS IDS: o e2e (`frontend/e2e/apoio.ts`) acha a tela por
|
||||
`kc-otp-login-form`, preenche `otp`, clica `kc-login` e detecta codigo
|
||||
recusado por `input-error-otp-code`. O Keycloak le `otp` e, com mais de um
|
||||
celular cadastrado, `selectedCredentialId`.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section>
|
||||
<#if section = "selo">
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2.5" width="12" height="19" rx="2.5"/><path d="M11 18.5h2"/><path d="M9.5 9.5h5"/><path d="M9.5 12.5h5"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
Código de <span class="hl">verificação</span>
|
||||
<#elseif section = "sub">
|
||||
Abra o aplicativo autenticador do seu celular e digite o código que ele mostra agora.
|
||||
<#elseif section = "form">
|
||||
<form id="kc-otp-login-form" action="${url.loginAction}" method="post" novalidate>
|
||||
<#if otpLogin.userOtpCredentials?size gt 1>
|
||||
<div class="cartoes credenciais" role="radiogroup" aria-label="Celular">
|
||||
<#list otpLogin.userOtpCredentials as otpCredential>
|
||||
<div>
|
||||
<input id="kc-otp-credential-${otpCredential?index}" type="radio" name="selectedCredentialId"
|
||||
value="${otpCredential.id}" <#if otpCredential.id == otpLogin.selectedCredentialId>checked="checked"</#if>>
|
||||
<label for="kc-otp-credential-${otpCredential?index}" class="cartao">
|
||||
<span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2.5" width="12" height="19" rx="2.5"/><path d="M11 18.5h2"/></svg></span>
|
||||
<span class="txt"><b>${otpCredential.userLabel}</b></span>
|
||||
</label>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="campo">
|
||||
<label class="rotulo" for="otp">${msg("loginOtpOneTime")}</label>
|
||||
<div class="caixa">
|
||||
<span class="icone">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="11" rx="2.4"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg>
|
||||
</span>
|
||||
<input id="otp" name="otp" type="text" autocomplete="one-time-code" inputmode="numeric" autofocus
|
||||
placeholder="000000" dir="ltr" class="codigo<#if messagesPerField.existsError('totp')> erro</#if>"
|
||||
aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>"/>
|
||||
</div>
|
||||
<#if messagesPerField.existsError('totp')>
|
||||
<span id="input-error-otp-code" class="erro-campo" aria-live="polite">${kcSanitize(messagesPerField.get('totp'))?no_esc}</span>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
<button class="botao entrar" name="login" id="kc-login" type="submit">
|
||||
${msg("doLogIn")}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
webauthn-register.ftl: |-
|
||||
<#--
|
||||
CADASTRAR PASSKEY / CHAVE DE SEGURANCA — acoes obrigatorias
|
||||
`webauthn-register-passwordless` (passkey, que dispensa a senha) e
|
||||
`webauthn-register` (chave como segundo fator). As duas usam esta pagina.
|
||||
|
||||
Ate 2026-10-08 era a pagina do tema BASE na moldura deste: "Registrar Chave de
|
||||
Seguranca" e um botao "Cadastre-se" — que parece criar CONTA, nao cadastrar a
|
||||
chave.
|
||||
|
||||
O <script type="module"> e o do base, sem mudar uma linha: ele importa
|
||||
`webauthnRegister.js` do tema base (que importa `rfc4648` pelo importmap do
|
||||
template.ftl), chama o navegador e posta o form `register`. Os ids dos campos
|
||||
escondidos e do botao `registerWebAuthn` sao lidos por esse JS.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout; section>
|
||||
<#if section = "selo">
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 4.8A8.5 8.5 0 0 1 20.5 12c0 1.5-.2 3-.6 4.4"/><path d="M3.5 12a8.5 8.5 0 0 1 1.6-5"/><path d="M6.8 18.6A8.4 8.4 0 0 1 3.5 12"/><path d="M8 12a4 4 0 0 1 8 0c0 3-.6 5.6-1.8 7.8"/><path d="M12 12c0 3.4-.9 6.4-2.6 8.7"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
Cadastre sua <span class="hl">chave de segurança</span>
|
||||
<#elseif section = "sub">
|
||||
Nos próximos logins, você confirma que é você com a digital, o rosto ou o PIN deste aparelho (ou com uma chave física).
|
||||
<#elseif section = "form">
|
||||
<form id="register" action="${url.loginAction}" method="post">
|
||||
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
|
||||
<input type="hidden" id="attestationObject" name="attestationObject"/>
|
||||
<input type="hidden" id="publicKeyCredentialId" name="publicKeyCredentialId"/>
|
||||
<input type="hidden" id="authenticatorLabel" name="authenticatorLabel"/>
|
||||
<input type="hidden" id="transports" name="transports"/>
|
||||
<input type="hidden" id="authenticatorAttachment" name="authenticatorAttachment"/>
|
||||
<input type="hidden" id="error" name="error"/>
|
||||
<label class="lembrar">
|
||||
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on"> ${msg("logoutOtherSessions")}
|
||||
</label>
|
||||
</form>
|
||||
|
||||
<script type="module">
|
||||
<#outputformat "JavaScript">
|
||||
import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js";
|
||||
const registerButton = document.getElementById('registerWebAuthn');
|
||||
registerButton.addEventListener("click", function() {
|
||||
const input = {
|
||||
challenge : ${challenge?c},
|
||||
userid : ${userid?c},
|
||||
username : ${username?c},
|
||||
signatureAlgorithms : [<#list signatureAlgorithms as sigAlg>${sigAlg?c},</#list>],
|
||||
rpEntityName : ${rpEntityName?c},
|
||||
rpId : ${rpId?c},
|
||||
attestationConveyancePreference : ${attestationConveyancePreference?c},
|
||||
authenticatorAttachment : ${authenticatorAttachment?c},
|
||||
requireResidentKey : ${requireResidentKey?c},
|
||||
residentKey : ${residentKey?c},
|
||||
userVerificationRequirement : ${userVerificationRequirement?c},
|
||||
createTimeout : ${createTimeout?c},
|
||||
excludeCredentialIds : ${excludeCredentialIds?c},
|
||||
initLabel : ${msg("webauthn-registration-init-label")?c},
|
||||
initLabelPrompt : ${msg("webauthn-registration-init-label-prompt")?c},
|
||||
errmsg : ${msg("webauthn-unsupported-browser-text")?c}
|
||||
};
|
||||
registerByWebAuthn(input);
|
||||
}, { once: true });
|
||||
</#outputformat>
|
||||
</script>
|
||||
|
||||
<button type="button" class="botao entrar" id="registerWebAuthn" style="margin-top:20px">
|
||||
Cadastrar chave de segurança
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
|
||||
</button>
|
||||
|
||||
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
|
||||
<form action="${url.loginAction}" id="kc-webauthn-settings-form" method="post" style="margin-top:0">
|
||||
<button type="submit" class="botao entrar secundario" id="cancelWebAuthnAIA" name="cancel-aia" value="true">${msg("doCancel")}</button>
|
||||
</form>
|
||||
</#if>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
webauthn-authenticate.ftl: |-
|
||||
<#--
|
||||
ENTRAR COM PASSKEY / CHAVE DE SEGURANCA — a etapa WebAuthn de um fluxo
|
||||
(quando o Keycloak pede a chave numa tela propria, e nao pela sugestao no
|
||||
campo de e-mail do login.ftl).
|
||||
|
||||
Ate 2026-10-08 era a pagina do tema BASE na moldura deste: a lista de chaves
|
||||
vinha com classes do PatternFly que o tema nao carrega, e saia como texto
|
||||
corrido.
|
||||
|
||||
O <script type="module"> e o do base, sem mudar uma linha (importa
|
||||
`webauthnAuthenticate.js` do tema base; o `rfc4648` vem do importmap do
|
||||
template.ftl). O JS le os forms `webauthn` e `authn_select` e o botao
|
||||
`authenticateWebAuthnButton` pelos ids.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout displayInfo=(realm.registrationAllowed && !registrationDisabled??); section>
|
||||
<#if section = "selo">
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 4.8A8.5 8.5 0 0 1 20.5 12c0 1.5-.2 3-.6 4.4"/><path d="M3.5 12a8.5 8.5 0 0 1 1.6-5"/><path d="M6.8 18.6A8.4 8.4 0 0 1 3.5 12"/><path d="M8 12a4 4 0 0 1 8 0c0 3-.6 5.6-1.8 7.8"/><path d="M12 12c0 3.4-.9 6.4-2.6 8.7"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
Entrar com <span class="hl">chave de segurança</span>
|
||||
<#elseif section = "sub">
|
||||
Confirme que é você com a digital, o rosto ou o PIN do aparelho (ou com a sua chave física).
|
||||
<#elseif section = "form">
|
||||
<div id="kc-form-webauthn">
|
||||
<form id="webauth" action="${url.loginAction}" method="post" style="display:none">
|
||||
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
|
||||
<input type="hidden" id="authenticatorData" name="authenticatorData"/>
|
||||
<input type="hidden" id="signature" name="signature"/>
|
||||
<input type="hidden" id="credentialId" name="credentialId"/>
|
||||
<input type="hidden" id="userHandle" name="userHandle"/>
|
||||
<input type="hidden" id="error" name="error"/>
|
||||
</form>
|
||||
|
||||
<#if authenticators??>
|
||||
<form id="authn_select" style="display:none">
|
||||
<#list authenticators.authenticators as authenticator>
|
||||
<input type="hidden" name="authn_use_chk" value="${authenticator.credentialId}"/>
|
||||
</#list>
|
||||
</form>
|
||||
|
||||
<#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
|
||||
<#if authenticators.authenticators?size gt 1>
|
||||
<p class="rotulo">${msg("webauthn-available-authenticators")}</p>
|
||||
</#if>
|
||||
<div class="cartoes" style="margin-bottom:18px">
|
||||
<#list authenticators.authenticators as authenticator>
|
||||
<div id="kc-webauthn-authenticator-item-${authenticator?index}" class="cartao estatico">
|
||||
<span class="ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4"/><path d="m10.8 12.2 8.7-8.7"/><path d="m17 6 2.5 2.5"/><path d="m14.5 8.5 2 2"/></svg></span>
|
||||
<span class="txt">
|
||||
<b id="kc-webauthn-authenticator-label-${authenticator?index}">${authenticator.label}</b>
|
||||
<#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content>
|
||||
<span id="kc-webauthn-authenticator-transport-${authenticator?index}"><#list authenticator.transports.displayNameProperties as nameProperty>${msg(nameProperty)}<#sep>, </#list></span>
|
||||
</#if>
|
||||
<span><span id="kc-webauthn-authenticator-createdlabel-${authenticator?index}">${msg('webauthn-createdAt-label')}</span>
|
||||
<span id="kc-webauthn-authenticator-created-${authenticator?index}">${authenticator.createdAt}</span></span>
|
||||
</span>
|
||||
</div>
|
||||
</#list>
|
||||
</div>
|
||||
</#if>
|
||||
</#if>
|
||||
|
||||
<button id="authenticateWebAuthnButton" type="button" autofocus class="botao entrar">
|
||||
${msg("webauthn-doAuthenticate")}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
<#outputformat "JavaScript">
|
||||
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
|
||||
const authButton = document.getElementById('authenticateWebAuthnButton');
|
||||
authButton.addEventListener("click", function() {
|
||||
const input = {
|
||||
isUserIdentified : ${isUserIdentified},
|
||||
challenge : ${challenge?c},
|
||||
userVerification : ${userVerification?c},
|
||||
rpId : ${rpId?c},
|
||||
createTimeout : ${createTimeout?c},
|
||||
errmsg : ${msg("webauthn-unsupported-browser-text")?c}
|
||||
};
|
||||
authenticateByWebAuthn(input);
|
||||
}, { once: true });
|
||||
</#outputformat>
|
||||
</script>
|
||||
<#elseif section = "info">
|
||||
<#if realm.registrationAllowed && !registrationDisabled??>
|
||||
<div id="kc-registration">
|
||||
<span>${msg("noAccount")} <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a></span>
|
||||
</div>
|
||||
</#if>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
select-authenticator.ftl: |-
|
||||
<#--
|
||||
ESCOLHER COMO ENTRAR — o "Tente outra forma" de um fluxo com alternativas
|
||||
(senha, passkey, codigo do celular...).
|
||||
|
||||
Ate 2026-10-08 era a pagina do tema BASE na moldura deste: cada opcao e um
|
||||
<button>, e o `.conteudo button` do tema (o botao de acao, laranja) pintava
|
||||
TODAS de laranja, com os icones do PatternFly — que o tema nao carrega — em
|
||||
branco. Aqui cada opcao e um cartao branco com icone proprio.
|
||||
|
||||
O Keycloak le `authenticationExecution` (o value do botao clicado). O id do
|
||||
form `kc-select-credential-form` e o do base.
|
||||
|
||||
O icone vem do `iconCssClass` da opcao (kcAuthenticatorPasswordClass,
|
||||
kcAuthenticatorOTPClass, kcAuthenticatorWebAuthnPasswordlessClass...): o nome
|
||||
da classe diz o tipo, e e so isso que se usa dela.
|
||||
-->
|
||||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout displayInfo=false; section>
|
||||
<#if section = "selo">
|
||||
<span class="selo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4"/><path d="m10.8 12.2 8.7-8.7"/><path d="m17 6 2.5 2.5"/><path d="m14.5 8.5 2 2"/></svg></span>
|
||||
<#elseif section = "header">
|
||||
Como você quer <span class="hl">entrar?</span>
|
||||
<#elseif section = "sub">
|
||||
${msg("loginChooseAuthenticator")}
|
||||
<#elseif section = "form">
|
||||
<form id="kc-select-credential-form" action="${url.loginAction}" method="post">
|
||||
<div class="cartoes">
|
||||
<#list auth.authenticationSelections as authenticationSelection>
|
||||
<#assign tipo = (authenticationSelection.iconCssClass!'')?lower_case>
|
||||
<button class="cartao" type="submit" name="authenticationExecution" value="${authenticationSelection.authExecId}">
|
||||
<span class="ico">
|
||||
<#if tipo?contains("passkey") || tipo?contains("webauthn")>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 4.8A8.5 8.5 0 0 1 20.5 12c0 1.5-.2 3-.6 4.4"/><path d="M3.5 12a8.5 8.5 0 0 1 1.6-5"/><path d="M6.8 18.6A8.4 8.4 0 0 1 3.5 12"/><path d="M8 12a4 4 0 0 1 8 0c0 3-.6 5.6-1.8 7.8"/><path d="M12 12c0 3.4-.9 6.4-2.6 8.7"/></svg>
|
||||
<#elseif tipo?contains("otp")>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="2.5" width="12" height="19" rx="2.5"/><path d="M11 18.5h2"/></svg>
|
||||
<#elseif tipo?contains("recovery")>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="4" width="14" height="17" rx="2"/><path d="M9 9h6"/><path d="M9 13h6"/><path d="M9 17h3"/></svg>
|
||||
<#elseif tipo?contains("password")>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="11" rx="2.4"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/></svg>
|
||||
<#else>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 4.5 6v5.5c0 4.6 3.1 7.9 7.5 9.5 4.4-1.6 7.5-4.9 7.5-9.5V6L12 3Z"/></svg>
|
||||
</#if>
|
||||
</span>
|
||||
<span class="txt">
|
||||
<b>${msg('${authenticationSelection.displayName}')}</b>
|
||||
<span>${msg('${authenticationSelection.helpText}')}</span>
|
||||
</span>
|
||||
<span class="seta"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></svg></span>
|
||||
</button>
|
||||
</#list>
|
||||
</div>
|
||||
</form>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user