mirror of https://github.com/halo-dev/halo
Add frontend support for customizing login and logout pages
Signed-off-by: JohnNiang <johnniang@foxmail.com>pull/6488/head
parent
1f9610686b
commit
8547ffe613
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -0,0 +1 @@
|
|||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" viewBox="0 0 2144 877" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="mb-8 flex-none"><linearGradient id="uicons-hepky0qzdn" gradientTransform="matrix(0 -848.921 848.921 0 1308.8 875.397)" gradientUnits="userSpaceOnUse" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#0050b5"></stop><stop offset="1" stop-color="#0b87fd"></stop></linearGradient><linearGradient id="uicons-9a00r6vh1r" gradientTransform="matrix(0 472.459 -473.895 0 587.619 -.861651)" gradientUnits="userSpaceOnUse" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#0048af"></stop><stop offset="1" stop-color="#003580"></stop></linearGradient><linearGradient id="uicons-payvanzieq" gradientTransform="matrix(0 898.506 -901.236 0 162.421 -12.1337)" gradientUnits="userSpaceOnUse" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#0b89ff"></stop><stop offset="1" stop-color="#004eb2"></stop></linearGradient><g fill="url(#uicons-hepky0qzdn)"><path d="m1028.16 339.331c148.249 0 268.609 120.36 268.609 268.609s-120.36 268.608-268.609 268.608-268.608-120.359-268.608-268.608 120.359-268.609 268.608-268.609zm0 119.152c82.488 0 149.457 66.969 149.457 149.457 0 82.487-66.969 149.456-149.457 149.456-82.487 0-149.456-66.969-149.456-149.456 0-82.488 66.969-149.457 149.456-149.457z"></path><path d="m1874.58 339.331c148.249 0 268.608 120.36 268.608 268.609s-120.359 268.608-268.608 268.608-268.609-120.359-268.609-268.608 120.36-268.609 268.609-268.609zm0 119.152c82.487 0 149.456 66.969 149.456 149.457 0 82.487-66.969 149.456-149.456 149.456-82.488 0-149.457-66.969-149.457-149.456 0-82.488 66.969-149.457 149.457-149.457z"></path><path d="m1309.27 377.585c0-10.083-7.222-18.719-17.146-20.504-19.618-3.528-51.9-9.334-74.172-13.34-6.073-1.092-12.318.564-17.052 4.522-4.734 3.959-7.469 9.812-7.469 15.983v491.469c0 5.525 2.195 10.824 6.102 14.731s9.206 6.102 14.731 6.102h74.173c5.525 0 10.824-2.195 14.731-6.102s6.102-9.206 6.102-14.731c0-84.425 0-400.286 0-478.13z"></path><path d="m1542.59 72.033c0-8.288-3.292-16.237-9.153-22.097-5.86-5.861-13.809-9.153-22.097-9.153-23.867 0-56.609 0-80.477 0-8.288 0-16.236 3.292-22.097 9.153-5.86 5.86-9.153 13.809-9.153 22.097v773.265c0 8.288 3.293 16.237 9.153 22.097 5.861 5.861 13.809 9.153 22.097 9.153h80.477c8.288 0 16.237-3.292 22.097-9.153 5.861-5.86 9.153-13.809 9.153-22.097 0-131.79 0-641.475 0-773.265z" fill-rule="nonzero"></path></g><path d="m506.409 822.063c0 13.815 5.494 27.062 15.271 36.821 9.777 9.76 23.034 15.23 36.848 15.206 18.674-.034 39.711-.072 58.369-.105 28.696-.052 51.932-23.329 51.932-52.026v-769.586c0-13.798-5.481-27.031-15.238-36.788-9.756-9.757-22.99-15.238-36.788-15.238h-58.368c-13.798 0-27.031 5.481-36.788 15.238s-15.238 22.99-15.238 36.788z" fill="url(#uicons-9a00r6vh1r)" fill-rule="nonzero"></path><path d="m616.746 322.662c13.813 0 27.061 5.487 36.829 15.255 9.767 9.768 15.254 23.015 15.254 36.829v447.062c0 13.814-5.487 27.061-15.254 36.829-9.768 9.767-23.016 15.255-36.829 15.255-18.632 0-39.622 0-58.254 0-13.813 0-27.061-5.488-36.828-15.255-9.768-9.768-15.255-23.015-15.255-36.829 0-68.223 0-187.159 0-255.383 0-13.813-5.487-27.061-15.255-36.828-9.767-9.768-23.015-15.255-36.828-15.255-129.249 0-454.326 0-454.326 0v-191.68z" fill="#0051b0" fill-rule="nonzero"></path><path d="m0 822.101c0 13.817 5.497 27.067 15.277 36.827 9.781 9.76 23.043 15.229 36.86 15.199 18.675-.04 39.713-.085 58.368-.124 28.69-.062 51.916-23.337 51.916-52.027 0-155.205 0-614.509 0-769.714 0-28.69-23.226-51.965-51.916-52.026-18.655-.04-39.693-.085-58.368-.125-13.817-.029-27.079 5.439-36.86 15.199-9.78 9.76-15.277 23.01-15.277 36.827v769.964z" fill="url(#uicons-payvanzieq)" fill-rule="nonzero"></path></svg>
|
After Width: | Height: | Size: 3.7 KiB |
|
@ -0,0 +1,154 @@
|
|||
const Toast = (function () {
|
||||
let container;
|
||||
|
||||
function getContainer() {
|
||||
if (container) return container;
|
||||
|
||||
container = document.createElement("div");
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
if (document.body) {
|
||||
document.body.appendChild(container);
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
class ToastMessage {
|
||||
constructor(message, type) {
|
||||
this.message = message;
|
||||
this.type = type;
|
||||
this.element = null;
|
||||
this.create();
|
||||
}
|
||||
|
||||
create() {
|
||||
this.element = document.createElement("div");
|
||||
this.element.textContent = this.message;
|
||||
this.element.style.cssText = `
|
||||
background-color: ${this.type === "success" ? "#4CAF50" : "#F44336"};
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
`;
|
||||
getContainer().appendChild(this.element);
|
||||
|
||||
setTimeout(() => {
|
||||
this.element.style.opacity = "1";
|
||||
}, 10);
|
||||
|
||||
setTimeout(() => {
|
||||
this.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.element.style.opacity = "0";
|
||||
setTimeout(() => {
|
||||
const parent = this.element.parentNode;
|
||||
if (parent) {
|
||||
parent.removeChild(this.element);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type) {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
new ToastMessage(message, type);
|
||||
});
|
||||
} else {
|
||||
new ToastMessage(message, type);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: function (message) {
|
||||
showToast(message, "success");
|
||||
},
|
||||
error: function (message) {
|
||||
showToast(message, "error");
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
function sendVerificationCode(button, sendRequest) {
|
||||
let timer;
|
||||
const countdown = 60;
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
button.disabled = true;
|
||||
sendRequest()
|
||||
.then(() => {
|
||||
startCountdown();
|
||||
Toast.success("发送成功");
|
||||
})
|
||||
.catch((e) => {
|
||||
button.disabled = false;
|
||||
if (e instanceof Error) {
|
||||
Toast.error(e.message);
|
||||
} else {
|
||||
Toast.error("发送失败,请稍后再试");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function startCountdown() {
|
||||
let remainingTime = countdown;
|
||||
button.disabled = true;
|
||||
button.classList.add("disabled");
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (remainingTime > 0) {
|
||||
button.textContent = `${remainingTime}s`;
|
||||
remainingTime--;
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
button.textContent = "Send";
|
||||
button.disabled = false;
|
||||
button.classList.remove("disabled");
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const passwordContainers = document.querySelectorAll(".toggle-password-display-flag");
|
||||
|
||||
passwordContainers.forEach((container) => {
|
||||
const passwordInput = container.querySelector('input[type="password"]');
|
||||
const toggleButton = container.querySelector(".toggle-password-button");
|
||||
const displayIcon = container.querySelector(".password-display-icon");
|
||||
const hiddenIcon = container.querySelector(".password-hidden-icon");
|
||||
|
||||
if (passwordInput && toggleButton && displayIcon && hiddenIcon) {
|
||||
toggleButton.addEventListener("click", () => {
|
||||
if (passwordInput.type === "password") {
|
||||
passwordInput.type = "text";
|
||||
displayIcon.style.display = "none";
|
||||
hiddenIcon.style.display = "block";
|
||||
} else {
|
||||
passwordInput.type = "password";
|
||||
displayIcon.style.display = "block";
|
||||
hiddenIcon.style.display = "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
/* Base */
|
||||
.gateway-page {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.gateway-wrapper,
|
||||
.gateway-wrapper:before,
|
||||
.gateway-wrapper:after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.gateway-wrapper *,
|
||||
.gateway-wrapper *:before,
|
||||
.gateway-wrapper *:after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.gateway-wrapper {
|
||||
--color-primary: #4ccba0;
|
||||
--color-secondary: #0e1731;
|
||||
--color-link: #1f75cb;
|
||||
--color-text: #374151;
|
||||
--color-border: #d1d5db;
|
||||
--rounded-sm: 0.125em;
|
||||
--rounded-base: 0.25em;
|
||||
--rounded-lg: 0.5em;
|
||||
--spacing-xl: 1.25em;
|
||||
--spacing-lg: 1em;
|
||||
--spacing-md: 0.875em;
|
||||
--spacing-sm: 0.5em;
|
||||
--text-md: 0.875em;
|
||||
}
|
||||
|
||||
.gateway-wrapper {
|
||||
margin: 0 auto;
|
||||
max-width: 28em;
|
||||
padding: 5% 1em;
|
||||
font-family:
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
Segoe UI,
|
||||
Roboto,
|
||||
Helvetica Neue,
|
||||
Arial,
|
||||
Noto Sans,
|
||||
sans-serif,
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
Segoe UI Symbol,
|
||||
"Noto Color Emoji";
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.halo-form-wrapper {
|
||||
border-radius: var(--rounded-lg);
|
||||
background: #fff;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
all: unset;
|
||||
margin-bottom: 1em;
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
.halo-form .form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1.3em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.halo-form .form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.halo-form .form-item-group {
|
||||
gap: var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1.3em;
|
||||
}
|
||||
|
||||
.halo-form .form-item-group .form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.halo-form .form-input {
|
||||
border-radius: var(--rounded-base);
|
||||
border: 1px solid var(--color-border);
|
||||
height: 2.5em;
|
||||
background: #fff;
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.halo-form .form-input:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: "2px";
|
||||
}
|
||||
|
||||
.halo-form .form-item input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
display: block;
|
||||
font-size: 1em;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.halo-form .form-item input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.halo-form .form-input-stack {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.halo-form .form-input-stack-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.halo-form .form-input-stack-select {
|
||||
all: unset;
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-md);
|
||||
padding-right: 1.85em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m12 13.171l4.95-4.95l1.414 1.415L12 16L5.636 9.636L7.05 8.222z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.3em center;
|
||||
}
|
||||
|
||||
.halo-form .form-input-stack-text {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
|
||||
.halo-form .form-item label {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.halo-form .form-item .form-label-group {
|
||||
margin-bottom: 0.5em;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.halo-form .form-item .form-label-group label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.halo-form .form-item-extra-link {
|
||||
color: var(--color-link);
|
||||
font-size: var(--text-md);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.halo-form .form-item-compact {
|
||||
gap: var(--spacing-sm);
|
||||
margin-bottom: 1.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.halo-form .form-item-compact label {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
|
||||
.halo-form button[type="submit"] {
|
||||
background: var(--color-secondary);
|
||||
border-radius: var(--rounded-base);
|
||||
height: 2.5em;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.halo-form button[type="submit"]:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.halo-form button[type="submit"]:active {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.halo-form button[disabled] {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.halo-form input[type="checkbox"] {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--rounded-sm);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
padding: 0;
|
||||
print-color-adjust: exact;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-origin: border-box;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
color: #2563eb;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.halo-form input[type="checkbox"]:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow:
|
||||
rgb(255, 255, 255) 0px 0px 0px 2px,
|
||||
rgb(37, 99, 235) 0px 0px 0px 4px,
|
||||
rgba(0, 0, 0, 0) 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.halo-form input[type="checkbox"]:checked {
|
||||
border-color: transparent;
|
||||
background-color: currentColor;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.halo-form .form-input-group {
|
||||
gap: var(--spacing-sm);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.halo-form .form-input {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.halo-form .form-input-group button {
|
||||
border-radius: var(--rounded-base);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-md);
|
||||
grid-column: span 1 / span 1;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.halo-form .form-input-group button:hover {
|
||||
color: #333;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.halo-form .form-input-group button:active {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.auth-provider-items {
|
||||
all: unset;
|
||||
gap: var(--spacing-md);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-provider-items li {
|
||||
all: unset;
|
||||
border-radius: var(--rounded-lg);
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 0.15s;
|
||||
}
|
||||
|
||||
.auth-provider-items li a {
|
||||
gap: var(--spacing-sm);
|
||||
padding: 0.7em 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #1f2937;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.auth-provider-items li img {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
.auth-provider-items li:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.auth-provider-items li:hover a {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.auth-provider-items li:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.divider-wrapper {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-md);
|
||||
gap: var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
|
||||
.divider-wrapper hr {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: var(--rounded-base);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
font-size: var(--text-md);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.alert::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
background: #d1d5db;
|
||||
width: 0.25em;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
border-color: #fde047;
|
||||
}
|
||||
|
||||
.alert-warning::before {
|
||||
background: #ea580c;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
border-color: #fca5a5;
|
||||
}
|
||||
|
||||
.alert-error::before {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
border-color: #86efac;
|
||||
}
|
||||
|
||||
.alert-success::before {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
border-color: #7dd3fc;
|
||||
}
|
||||
|
||||
.alert-info::before {
|
||||
background: #0284c7;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.halo-form input[type="checkbox"]:checked {
|
||||
-webkit-appearance: auto;
|
||||
-moz-appearance: auto;
|
||||
appearance: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.halo-form .form-item-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<!doctype html>
|
||||
<html
|
||||
xmlns:th="https://www.thymeleaf.org"
|
||||
th:replace="~{gateway_modules/layout :: layout(title = |#{title} - ${site.title}|, head = null, body = ~{::body})}"
|
||||
>
|
||||
<th:block th:fragment="body">
|
||||
<div class="gateway-wrapper">
|
||||
<div th:replace="~{gateway_modules/common_fragments::haloLogo}"></div>
|
||||
<div class="halo-form-wrapper">
|
||||
<h1 class="form-title" th:text="#{title}"></h1>
|
||||
<form
|
||||
class="halo-form"
|
||||
th:action="@{/challenges/two-factor/totp}"
|
||||
name="two-factor-form"
|
||||
id="two-factor-form"
|
||||
method="post"
|
||||
>
|
||||
<div class="alert alert-error" role="alert" th:if="${param.error.size() > 0}">
|
||||
<strong th:text="#{messages.invalidError}"></strong>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label for="code" th:text="#{form.code.label}"></label>
|
||||
<div class="form-input">
|
||||
<input
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
id="code"
|
||||
name="code"
|
||||
autocomplete="one-time-code"
|
||||
pattern="\d{6}"
|
||||
autofocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<button type="submit" th:text="#{form.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</html>
|
|
@ -0,0 +1,4 @@
|
|||
title=两步验证
|
||||
messages.invalidError=错误的验证码
|
||||
form.code.label=验证码
|
||||
form.submit=验证
|
|
@ -0,0 +1,4 @@
|
|||
title=Two-Factor Authentication
|
||||
messages.invalidError=Invalid TOTP code
|
||||
form.code.label=TOTP Code
|
||||
form.submit=Verify
|
|
@ -0,0 +1,110 @@
|
|||
<th:block th:fragment="basicStaticResources">
|
||||
<script th:inline="javascript">
|
||||
const resources = {
|
||||
title: `[(#{title})]`,
|
||||
};
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="/webjars/normalize.css/8.0.1/normalize.css" />
|
||||
<link rel="stylesheet" th:href="|/styles/main.css?v=${site.version}|" />
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
</th:block>
|
||||
|
||||
<div th:remove="tag" th:fragment="languageSwitcher">
|
||||
<style>
|
||||
.language-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 2em 0;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.language-switcher label {
|
||||
color: var(--color-text);
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.language-switcher select {
|
||||
all: unset;
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: var(--text-md);
|
||||
height: 2em;
|
||||
border-radius: var(--rounded-lg);
|
||||
outline: none;
|
||||
padding: 0 2em 0 0.5em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-text);
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='m12 13.171l4.95-4.95l1.414 1.415L12 16L5.636 9.636L7.05 8.222z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.45rem center;
|
||||
}
|
||||
|
||||
.language-switcher select:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
<div class="language-switcher">
|
||||
<label>
|
||||
<svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m12.87 15.07l-2.54-2.51l.03-.03A17.5 17.5 0 0 0 14.07 6H17V4h-7V2H8v2H1v2h11.17C11.5 7.92 10.44 9.75 9 11.35C8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5l3.11 3.11zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2zm-2.62 7l1.62-4.33L19.12 17z"
|
||||
></path>
|
||||
</svg>
|
||||
</label>
|
||||
<select id="language-select" onchange="changeLanguage()">
|
||||
<option value="en" th:selected="${#locale.toLanguageTag} == 'en'">English</option>
|
||||
<option value="es" th:selected="${#locale.toLanguageTag} == 'es'">Español</option>
|
||||
<option value="zh-CN" th:selected="${#locale.toLanguageTag} == 'zh-CN'">简体中文</option>
|
||||
<option value="zh-TW" th:selected="${#locale.toLanguageTag} == 'zh-TW'">繁体中文</option>
|
||||
</select>
|
||||
<script type="text/javascript">
|
||||
function changeLanguage() {
|
||||
const selectedLanguage = document.getElementById("language-select").value;
|
||||
const currentURL = new URL(window.location.href);
|
||||
currentURL.searchParams.set("language", selectedLanguage);
|
||||
window.location.href = currentURL.toString();
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:remove="tag" th:fragment="haloLogo">
|
||||
<style>
|
||||
.halo-logo {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.halo-logo img {
|
||||
width: 6em;
|
||||
}
|
||||
</style>
|
||||
<div class="halo-logo">
|
||||
<img src="/images/wordmark.svg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:remove="tag" th:fragment="socialAuthProviders">
|
||||
<th:block th:unless="${#lists.isEmpty(socialAuthProviders)}">
|
||||
<div class="divider-wrapper">
|
||||
<hr />
|
||||
<th:block th:text="#{socialLogin.label}"></th:block>
|
||||
<hr />
|
||||
</div>
|
||||
<ul class="auth-provider-items">
|
||||
<li th:each="provider : ${socialAuthProviders}">
|
||||
<a th:href="${provider.spec.authenticationUrl}">
|
||||
<img th:src="${provider.spec.logo}" />
|
||||
<span th:text="${provider.spec.displayName}"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</th:block>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
socialLogin.label=社交登录
|
|
@ -0,0 +1 @@
|
|||
socialLogin.label=Social Login
|
|
@ -0,0 +1,31 @@
|
|||
<div th:remove="tag" th:fragment="password(id,name,required,minlength,enableToggle)">
|
||||
<div class="form-input" th:classappend="${enableToggle ? 'form-input-stack toggle-password-display-flag' : ''}">
|
||||
<input
|
||||
th:id="${id}"
|
||||
th:name="${name}"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
th:required="${required}"
|
||||
th:minlength="${minlength}"
|
||||
/>
|
||||
|
||||
<div th:if="${enableToggle}" class="form-input-stack-icon toggle-password-button">
|
||||
<svg class="password-hidden-icon" style="display: none" viewBox="0 0 24 24" width="1em" height="1em">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 3c5.392 0 9.878 3.88 10.819 9c-.94 5.12-5.427 9-10.819 9c-5.392 0-9.878-3.88-10.818-9C2.122 6.88 6.608 3 12 3Zm0 16a9.005 9.005 0 0 0 8.778-7a9.005 9.005 0 0 0-17.555 0A9.005 9.005 0 0 0 12 19Zm0-2.5a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9Zm0-2a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5Z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<svg class="password-display-icon" viewBox="0 0 24 24" width="1em" height="1em">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.883 19.297A10.949 10.949 0 0 1 12 21c-5.392 0-9.878-3.88-10.818-9A10.982 10.982 0 0 1 4.52 5.935L1.394 2.808l1.414-1.414l19.799 19.798l-1.414 1.415l-3.31-3.31ZM5.936 7.35A8.965 8.965 0 0 0 3.223 12a9.005 9.005 0 0 0 13.201 5.838l-2.028-2.028A4.5 4.5 0 0 1 8.19 9.604L5.936 7.35Zm6.978 6.978l-3.242-3.241a2.5 2.5 0 0 0 3.241 3.241Zm7.893 2.265l-1.431-1.431A8.935 8.935 0 0 0 20.778 12A9.005 9.005 0 0 0 9.552 5.338L7.974 3.76C9.221 3.27 10.58 3 12 3c5.392 0 9.878 3.88 10.819 9a10.947 10.947 0 0 1-2.012 4.593Zm-9.084-9.084a4.5 4.5 0 0 1 4.769 4.769l-4.77-4.77Z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html th:lang="${#locale.toLanguageTag}" th:fragment="layout (title,head,body)">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible" />
|
||||
<meta content="webkit" name="renderer" />
|
||||
<meta
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||
name="viewport"
|
||||
/>
|
||||
<meta content="noindex,nofollow" name="robots" />
|
||||
<title th:text="${title}"></title>
|
||||
|
||||
<th:block th:replace="~{gateway_modules/common_fragments::basicStaticResources}"></th:block>
|
||||
|
||||
<th:block th:if="${head != null}">
|
||||
<th:block th:replace="${head}" />
|
||||
</th:block>
|
||||
</head>
|
||||
<body class="gateway-page">
|
||||
<th:block th:replace="${body}" />
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,95 @@
|
|||
<!-- Those fragments are only for login template-->
|
||||
<form
|
||||
th:fragment="form"
|
||||
class="halo-form"
|
||||
name="login-form"
|
||||
id="login-form"
|
||||
th:action="${authProvider.spec.authenticationUrl}"
|
||||
th:method="${authProvider.spec.method}"
|
||||
>
|
||||
<div class="alert alert-error" role="alert" th:if="${param.error.size() > 0}" th:with="error = ${param.error[0]}">
|
||||
<strong th:if="${error == 'invalid-credential'}">
|
||||
<span th:text="#{error.invalid-credential}"></span>
|
||||
</strong>
|
||||
<strong th:if="${error == 'rate-limit-exceeded'}">
|
||||
<span th:text="#{error.rate-limit-exceeded}"></span>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="alert" role="alert" th:if="${param.logout.size() > 0}">
|
||||
<strong th:text="#{messages.logoutSuccess}"></strong>
|
||||
</div>
|
||||
<div class="alert" role="alert" th:if="${param.signup.size() > 0}">
|
||||
<strong th:text="#{messages.signupSuccess}"> </strong>
|
||||
</div>
|
||||
|
||||
<div th:replace="~{__${fragmentTemplateName}__::form}"></div>
|
||||
|
||||
<div th:if="${authProvider.spec.rememberMeSupport}" class="form-item-compact">
|
||||
<input type="checkbox" id="remember-me" name="remember-me" value="true"/>
|
||||
<label for="remember-me" th:text="#{form.rememberMe.label}"></label>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<button type="submit" th:text="#{form.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div th:remove="tag" th:fragment="formAuthProviders">
|
||||
<th:block th:unless="${#lists.isEmpty(formAuthProviders)}">
|
||||
<div class="divider-wrapper">
|
||||
<hr/>
|
||||
<th:block th:text="#{otherLogin.label}"></th:block>
|
||||
<hr/>
|
||||
</div>
|
||||
<ul class="auth-provider-items">
|
||||
<li th:each="provider : ${formAuthProviders}">
|
||||
<a th:href="'/login?method=' + ${provider.metadata.name}">
|
||||
<img th:src="${provider.spec.logo}"/>
|
||||
<span th:text="${provider.spec.displayName}"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
<div th:remove="tag" th:fragment="miscellaneous">
|
||||
<style>
|
||||
.signup-notice,
|
||||
.returntosite-notice {
|
||||
font-size: var(--text-md);
|
||||
color: var(--color-text);
|
||||
text-align: center;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.signup-notice a {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
.returntosite-notice a {
|
||||
color: var(--color-text);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3em;
|
||||
}
|
||||
|
||||
.returntosite-notice a,
|
||||
.signup-notice a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<div th:if="${globalInfo.allowRegistration}" class="signup-notice">
|
||||
<div>
|
||||
<th:block th:text="#{signup.description}"></th:block>
|
||||
<a th:href="@{/signup}" th:text="#{signup.link}"></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="returntosite-notice">
|
||||
<a th:href="@{/}">
|
||||
<svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
|
||||
<path fill="currentColor" d="M21 11H6.83l3.58-3.59L9 6l-6 6l6 6l1.41-1.42L6.83 13H21z"></path>
|
||||
</svg>
|
||||
<span th:text="#{returnToSite}"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
messages.loginError=无效的凭证。
|
||||
messages.logoutSuccess=登出成功。
|
||||
messages.signupSuccess=恭喜!注册成功,请立即登录。
|
||||
|
||||
error.invalid-credential=无效的凭证。
|
||||
error.rate-limit-exceeded=请求过于频繁,请稍后再试。
|
||||
|
||||
form.rememberMe.label=保持登录会话
|
||||
form.submit=登录
|
||||
otherLogin.label=其他登录方式
|
||||
signup.description=没有账号?
|
||||
signup.link=立即注册
|
||||
returnToSite=返回网站
|
|
@ -0,0 +1,13 @@
|
|||
messages.loginError=Invalid credentials.
|
||||
messages.logoutSuccess=Logout successfully.
|
||||
messages.signupSuccess=Congratulations! Sign up successfully, please sign in now.
|
||||
|
||||
error.invalid-credential=Invalid credentials.
|
||||
error.rate-limit-exceeded=Too many requests, please try again later.
|
||||
|
||||
form.rememberMe.label=Remember me
|
||||
form.submit=Login
|
||||
otherLogin.label=Other Login
|
||||
signup.description=Don't have an account?
|
||||
signup.link=Sign up
|
||||
returnToSite=Return to site
|
|
@ -0,0 +1,20 @@
|
|||
<!doctype html>
|
||||
<html
|
||||
xmlns:th="https://www.thymeleaf.org"
|
||||
th:replace="~{gateway_modules/layout :: layout(title = |#{title} - ${site.title}|, head = null, body = ~{::body})}"
|
||||
>
|
||||
<th:block th:fragment="body">
|
||||
<div class="gateway-wrapper">
|
||||
<div th:replace="~{gateway_modules/common_fragments::haloLogo}"></div>
|
||||
|
||||
<div class="halo-form-wrapper">
|
||||
<form th:replace="~{gateway_modules/login_fragments::form}"></form>
|
||||
<div th:replace="~{gateway_modules/login_fragments::formAuthProviders}"></div>
|
||||
<div th:replace="~{gateway_modules/common_fragments::socialAuthProviders}"></div>
|
||||
</div>
|
||||
|
||||
<div th:replace="~{gateway_modules/login_fragments::miscellaneous}"></div>
|
||||
<div th:replace="~{gateway_modules/common_fragments::languageSwitcher}"></div>
|
||||
</div>
|
||||
</th:block>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
title=登录
|
|
@ -0,0 +1,37 @@
|
|||
<div th:remove="tag" th:fragment="form">
|
||||
<div class="form-item">
|
||||
<label for="email"> Email </label>
|
||||
|
||||
<div class="form-input">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label for="code"> Code </label>
|
||||
<div class="form-input-group">
|
||||
<div class="form-input">
|
||||
<input
|
||||
type="text"
|
||||
id="code"
|
||||
name="code"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button type="button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
title=Login
|
|
@ -0,0 +1,56 @@
|
|||
<div th:remove="tag" th:fragment="form">
|
||||
<script src="/webjars/jsencrypt/3.3.2/bin/jsencrypt.min.js" defer></script>
|
||||
<script th:inline="javascript" type="text/javascript">
|
||||
const publicKey = /*[[${publicKey}]]*/ "";
|
||||
|
||||
// Encrypt function
|
||||
function encryptPassword(password) {
|
||||
const encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(publicKey);
|
||||
return encrypt.encrypt(password);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const loginForm = document.getElementById("login-form");
|
||||
loginForm.addEventListener("submit", function (event) {
|
||||
const passwordInput = document.getElementById("password");
|
||||
const password = passwordInput.value;
|
||||
passwordInput.value = encryptPassword(password);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="form-item">
|
||||
<label for="username" th:text="#{form.username.label}"> </label>
|
||||
|
||||
<div class="form-input">
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-label-group">
|
||||
<label for="password" th:text="#{form.password.label}"> </label>
|
||||
<a
|
||||
class="form-item-extra-link"
|
||||
tabindex="-1"
|
||||
th:href="@{/password-reset}"
|
||||
th:text="#{form.password.forgot}"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<th:block
|
||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = null, enableToggle = true)}"
|
||||
></th:block>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,3 @@
|
|||
form.username.label=用户名
|
||||
form.password.label=密码
|
||||
form.password.forgot=忘记密码?
|
|
@ -0,0 +1,3 @@
|
|||
form.username.label=Username
|
||||
form.password.label=Password
|
||||
form.password.forgot=Forgot your password?
|
|
@ -0,0 +1,18 @@
|
|||
<!doctype html>
|
||||
<html
|
||||
xmlns:th="https://www.thymeleaf.org"
|
||||
th:replace="~{gateway_modules/layout:: layout(title = |#{title} - ${site.title}|, head = null, body = ~{::body})}"
|
||||
>
|
||||
<th:block th:fragment="body">
|
||||
<div class="gateway-wrapper">
|
||||
<div class="halo-form-wrapper">
|
||||
<h1 class="form-title" th:text="#{form.title}"></h1>
|
||||
<form class="halo-form" id="logout-form" name="logout-form" th:action="@{/logout}" method="post">
|
||||
<div class="form-item">
|
||||
<button type="submit" th:text="#{form.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||
title=退出登录
|
||||
form.title=确定要退出登录吗?
|
||||
form.submit=退出登录
|
|
@ -0,0 +1,3 @@
|
|||
title=Logout
|
||||
form.title=Are you sure want to log out?
|
||||
form.submit=Logout
|
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<html
|
||||
xmlns:th="https://www.thymeleaf.org"
|
||||
th:replace="~{gateway_modules/layout:: layout(title = |#{title(${username})} - ${site.title}|, head = null, body = ~{::body})}"
|
||||
>
|
||||
<th:block th:fragment="body">
|
||||
<div class="gateway-wrapper">
|
||||
<div th:replace="~{gateway_modules/common_fragments::haloLogo}"></div>
|
||||
<div class="halo-form-wrapper">
|
||||
<h1 class="form-title" th:text="#{title(${username})}"></h1>
|
||||
<p style="color: red" role="alert" th:if="${error}" th:text="${error}"></p>
|
||||
<form
|
||||
class="halo-form"
|
||||
th:action="@{/password-reset/{resetToken}(resetToken=${resetToken})}"
|
||||
method="post"
|
||||
>
|
||||
<div class="form-item">
|
||||
<label for="password" th:text="#{form.password.label}">Password</label>
|
||||
<th:block
|
||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = 6, enableToggle = true)}"
|
||||
></th:block>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label for="confirmPassword" th:text="#{form.confirmPassword.label}">Confirm Password</label>
|
||||
<th:block
|
||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'confirmPassword', name = 'confirmPassword', required = 'true', minlength = 6, enableToggle = true)}"
|
||||
></th:block>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<p th:text="#{form.password.tips}"></p>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<button type="submit" th:text="#{form.submit}">Change password</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</html>
|
|
@ -0,0 +1,5 @@
|
|||
title=为 {0} 修改密码
|
||||
form.password.label=密码
|
||||
form.confirmPassword.label=确认密码
|
||||
form.password.tips=密码必须至少包含 8 个字符,并且至少包含一个大写字母、一个小写字母、一个数字和一个特殊字符。
|
||||
form.submit=修改密码
|
|
@ -0,0 +1,5 @@
|
|||
title=Change password for @{0}
|
||||
form.password.label=Password
|
||||
form.confirmPassword.label=Confirm Password
|
||||
form.password.tips=Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character.
|
||||
form.submit=Change password
|
|
@ -0,0 +1,36 @@
|
|||
<!doctype html>
|
||||
<html
|
||||
xmlns:th="https://www.thymeleaf.org"
|
||||
th:replace="~{gateway_modules/layout:: layout(title = |#{title} - ${site.title}|, head = null, body = ~{::body})}"
|
||||
>
|
||||
<th:block th:fragment="body">
|
||||
<div class="gateway-wrapper">
|
||||
<div th:replace="~{gateway_modules/common_fragments::haloLogo}"></div>
|
||||
<div class="halo-form-wrapper">
|
||||
<h1 class="form-title" th:text="${sent} ? #{sent.title} : #{title}"></h1>
|
||||
<div class="alert alert-error" th:if="${error}">
|
||||
<strong th:text="${error}"></strong>
|
||||
</div>
|
||||
<form th:if="${sent}" method="get" action="/login" class="halo-form">
|
||||
<div class="form-item">
|
||||
<div class="alert" th:text="#{sent.form.message}"></div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<button type="submit" th:text="#{sent.form.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
<form th:unless="${sent}" class="halo-form" th:action="@{/password-reset}" method="post">
|
||||
<div class="form-item">
|
||||
<label for="email" th:text="#{form.email.label}"></label>
|
||||
<div class="form-input">
|
||||
<input type="email" id="email" name="email" autofocus required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<button type="submit" th:text="#{form.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</th:block>
|
||||
</html>
|
|
@ -0,0 +1,6 @@
|
|||
title=重置密码
|
||||
form.email.label=电子邮箱
|
||||
form.submit=提交
|
||||
sent.form.submit=返回到登录页面
|
||||
sent.form.message=检查您的电子邮件中是否有重置密码的链接。如果几分钟内没有出现,请检查您的垃圾邮件文件夹。
|
||||
sent.title=已发送重置密码的邮件
|
|
@ -0,0 +1,6 @@
|
|||
title=Reset password
|
||||
form.email.label=Email
|
||||
form.submit=Submit
|
||||
sent.form.submit=Return to login
|
||||
sent.form.message=Check your email for a link to reset your password. If it doesn’t appear within a few minutes, check your spam folder.
|
||||
sent.title=Password reset email has been sent
|
|
@ -0,0 +1,202 @@
|
|||
<!doctype html>
|
||||
<html
|
||||
xmlns:th="https://www.thymeleaf.org"
|
||||
th:replace="~{gateway_modules/layout :: layout(title = |#{title} - ${site.title}|, head = ~{::head}, body = ~{::body})}"
|
||||
>
|
||||
<th:block th:fragment="head">
|
||||
<style>
|
||||
.signup-page-wrapper {
|
||||
max-width: 35em;
|
||||
}
|
||||
</style>
|
||||
</th:block>
|
||||
<th:block th:fragment="body">
|
||||
<div class="gateway-wrapper signup-page-wrapper">
|
||||
<div th:replace="~{gateway_modules/common_fragments::haloLogo}"></div>
|
||||
<div class="halo-form-wrapper">
|
||||
<h1 class="form-title" th:text="#{title}"></h1>
|
||||
<p class="alert alert=erro" role="alert" th:if="${error == 'invalid-email-code'}">
|
||||
<span th:text="#{error.invalid-email-code}">Invalid email code</span>
|
||||
</p>
|
||||
<p class="alert alert=erro" role="alert" th:if="${error == 'rate-limit-exceeded'}">
|
||||
<span th:text="#{error.rate-limit-exceeded}">Rate limit exceeded</span>
|
||||
</p>
|
||||
<p class="alert alert=erro" role="alert" th:if="${error == 'duplicate-name'}">
|
||||
<span th:text="#{error.duplicate-name}">Duplicate name</span>
|
||||
</p>
|
||||
<form class="halo-form" name="signup-form" id="signup-form" th:action="@{/signup}" th:object="${form}"
|
||||
method="post">
|
||||
<div class="form-item-group">
|
||||
<div class="form-item">
|
||||
<label for="username" th:text="#{form.username.label}"></label>
|
||||
<div class="form-input">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
autofocus
|
||||
required
|
||||
th:field="*{username}"
|
||||
/>
|
||||
</div>
|
||||
<p class="alert alert-error"
|
||||
th:if="${#fields.hasErrors('username')}"
|
||||
th:errors="*{username}">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<label for="displayName" th:text="#{form.displayName.label}"></label>
|
||||
<div class="form-input">
|
||||
<input
|
||||
type="text"
|
||||
id="displayName"
|
||||
name="displayName"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
required
|
||||
th:field="*{displayName}"
|
||||
/>
|
||||
</div>
|
||||
<p class="alert alert-error"
|
||||
th:if="${#fields.hasErrors('displayName')}"
|
||||
th:errors="*{displayName}">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item-group">
|
||||
<div class="form-item">
|
||||
<label for="email" th:text="#{form.email.label}"></label>
|
||||
<div class="form-input">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
required
|
||||
th:field="*{email}"
|
||||
/>
|
||||
</div>
|
||||
<p class="alert alert-error"
|
||||
th:if="${#fields.hasErrors('email')}"
|
||||
th:errors="*{email}">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-item" th:if="${globalInfo.mustVerifyEmailOnRegistration}">
|
||||
<label for="emailCode" th:text="#{form.emailCode.label}"></label>
|
||||
<div class="form-input-group">
|
||||
<div class="form-input">
|
||||
<input
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="\d*"
|
||||
id="emailCode"
|
||||
name="emailCode"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="emailCodeSendButton"
|
||||
type="button"
|
||||
th:text="#{form.emailCode.sendButton}"
|
||||
></button>
|
||||
</div>
|
||||
<p class="alert alert-error"
|
||||
th:if="${#fields.hasErrors('emailCode')}"
|
||||
th:errors="*{emailCode}">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<label for="password" th:text="#{form.password.label}"></label>
|
||||
<th:block
|
||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'password', name = 'password', required = 'true', minlength = 6, enableToggle = true)}"
|
||||
></th:block>
|
||||
<p class="alert alert-error"
|
||||
th:if="${#fields.hasErrors('password')}"
|
||||
th:errors="*{password}">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<label for="confirmPassword" th:text="#{form.confirmPassword.label}"></label>
|
||||
<th:block
|
||||
th:replace="~{gateway_modules/input_fragments :: password(id = 'confirmPassword', name = null, required = 'true', minlength = 6, enableToggle = true)}"
|
||||
></th:block>
|
||||
<p class="alert alert-error"
|
||||
th:if="${#fields.hasErrors('confirmPassword')}"
|
||||
th:errors="*{confirmPassword}">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<button type="submit" th:text="#{form.submit}"></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div th:replace="~{gateway_modules/common_fragments::socialAuthProviders}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script th:inline="javascript">
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function sendRequest() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const email = document.getElementById("email").value;
|
||||
|
||||
if (!email) {
|
||||
throw new Error("请先输入邮箱地址");
|
||||
}
|
||||
|
||||
fetch("/signup/send-email-code", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: email }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
[[${_csrf.headerName}]]: [[${_csrf.token}]],
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
resolve(response);
|
||||
}
|
||||
reject(response);
|
||||
})
|
||||
.catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const emailCodeSendButton = document.getElementById("emailCodeSendButton");
|
||||
sendVerificationCode(emailCodeSendButton, sendRequest);
|
||||
|
||||
var password = document.getElementById("password"),
|
||||
confirm_password = document.getElementById("confirmPassword");
|
||||
|
||||
function validatePassword() {
|
||||
if (password.value != confirm_password.value) {
|
||||
confirm_password.setCustomValidity("Passwords Don't Match");
|
||||
} else {
|
||||
confirm_password.setCustomValidity("");
|
||||
}
|
||||
}
|
||||
|
||||
password.onchange = validatePassword;
|
||||
confirm_password.onkeyup = validatePassword;
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</html>
|
|
@ -0,0 +1,13 @@
|
|||
title=注册
|
||||
form.username.label=用户名
|
||||
form.displayName.label=名称
|
||||
form.email.label=电子邮箱
|
||||
form.emailCode.label=邮箱验证码
|
||||
form.emailCode.sendButton=发送
|
||||
form.password.label=密码
|
||||
form.confirmPassword.label=确认密码
|
||||
form.submit=注册
|
||||
|
||||
error.invalid-email-code=无效的邮箱验证码
|
||||
error.duplicate-name=用户名已经被注册
|
||||
error.rate-limit-exceeded=请求过于频繁,请稍后再试
|
|
@ -0,0 +1,9 @@
|
|||
title=Sign up
|
||||
form.username.label=Username
|
||||
form.displayName.label=Display name
|
||||
form.email.label=Email
|
||||
form.emailCode.label=Email Code
|
||||
form.emailCode.sendButton=Send
|
||||
form.password.label=Password
|
||||
form.confirmPassword.label=Confirm password
|
||||
form.submit=Sign up
|
|
@ -57,7 +57,7 @@ const handleLogout = () => {
|
|||
document.cookie =
|
||||
"XSRF-TOKEN=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
||||
|
||||
router.replace({ name: "Login" });
|
||||
window.location.href = "/login";
|
||||
} catch (error) {
|
||||
console.error("Failed to logout", error);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { setupVueQuery } from "@/setup/setupVueQuery";
|
|||
import { useGlobalInfoStore } from "@/stores/global-info";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { getCookie } from "@/utils/cookie";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import {
|
||||
setupCoreModules,
|
||||
|
@ -78,8 +79,7 @@ async function initApp() {
|
|||
await userStore.fetchCurrentUser();
|
||||
|
||||
// set locale
|
||||
i18n.global.locale.value =
|
||||
localStorage.getItem("locale") || getBrowserLanguage();
|
||||
i18n.global.locale.value = getCookie("language") || getBrowserLanguage();
|
||||
|
||||
const globalInfoStore = useGlobalInfoStore();
|
||||
await globalInfoStore.fetchGlobalInfo();
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export function getCookie(name: string) {
|
||||
const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
|
||||
return match ? match[2] : null;
|
||||
}
|
|
@ -53,7 +53,7 @@ const handleLogout = () => {
|
|||
document.cookie =
|
||||
"XSRF-TOKEN=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
|
||||
|
||||
window.location.href = "/console/login";
|
||||
window.location.href = "/login";
|
||||
} catch (error) {
|
||||
console.error("Failed to logout", error);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { setupVueQuery } from "@/setup/setupVueQuery";
|
|||
import { useGlobalInfoStore } from "@/stores/global-info";
|
||||
import { useRoleStore } from "@/stores/role";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { getCookie } from "@/utils/cookie";
|
||||
import { hasPermission } from "@/utils/permission";
|
||||
import { consoleApiClient } from "@halo-dev/api-client";
|
||||
import router from "@uc/router";
|
||||
|
@ -66,8 +67,7 @@ async function initApp() {
|
|||
await userStore.fetchCurrentUser();
|
||||
|
||||
// set locale
|
||||
i18n.global.locale.value =
|
||||
localStorage.getItem("locale") || getBrowserLanguage();
|
||||
i18n.global.locale.value = getCookie("language") || getBrowserLanguage();
|
||||
|
||||
const globalInfoStore = useGlobalInfoStore();
|
||||
await globalInfoStore.fetchGlobalInfo();
|
||||
|
|
|
@ -13,7 +13,7 @@ export function setupAuthCheckGuard(router: Router) {
|
|||
const userStore = useUserStore();
|
||||
|
||||
if (userStore.isAnonymous) {
|
||||
window.location.href = `/console/login?redirect_uri=${encodeURIComponent(
|
||||
window.location.href = `/login?redirect_uri=${encodeURIComponent(
|
||||
window.location.href
|
||||
)}`;
|
||||
return;
|
||||
|
|
Loading…
Reference in New Issue