Add auth
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
.login-bounds {
|
||||
max-width: 500px;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
transition: border-color .2s;
|
||||
}
|
||||
|
||||
.form-input input {
|
||||
border: 0;
|
||||
background: none;
|
||||
width: 100%;
|
||||
height: 5.2rem;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.form-input-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-top: -0.1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.form-input input[type="password"] {
|
||||
letter-spacing: 0.3rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.form-input input[type="password"]::placeholder {
|
||||
letter-spacing: 0;
|
||||
font-size: var(--font-size-base);
|
||||
}
|
||||
|
||||
.form-input:hover {
|
||||
border-color: var(--color-progress-border);
|
||||
}
|
||||
|
||||
.form-input:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
transition-duration: .7s;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
background: none;
|
||||
border: 1px solid var(--color-text-subdue);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text-paragraph);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: var(--font-size-h4);
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all .3s, margin-top 0s;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.login-button:not(:disabled) {
|
||||
box-shadow: 0 0 10px 1px var(--color-separator);
|
||||
}
|
||||
|
||||
.login-error-message:not(:empty) + .login-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.login-button:focus, .login-button:hover {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
border-color: var(--color-separator);
|
||||
color: var(--color-text-subdue);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-button svg {
|
||||
width: 1.7rem;
|
||||
height: 1.7rem;
|
||||
transition: transform .2s;
|
||||
}
|
||||
|
||||
.login-button:not(:disabled):hover svg, .login-button:not(:disabled):focus svg {
|
||||
transform: translateX(.5rem);
|
||||
}
|
||||
|
||||
.animate-entrance {
|
||||
animation: fieldReveal 0.7s backwards;
|
||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.animate-entrance:nth-child(1) { animation-delay: .1s; }
|
||||
.animate-entrance:nth-child(2) { animation-delay: .2s; }
|
||||
.animate-entrance:nth-child(4) { animation-delay: .3s; }
|
||||
|
||||
@keyframes fieldReveal {
|
||||
from {
|
||||
opacity: 0.0001;
|
||||
transform: translateY(4rem);
|
||||
}
|
||||
}
|
||||
|
||||
.login-error-message {
|
||||
color: var(--color-negative);
|
||||
font-size: var(--font-size-base);
|
||||
padding: 1.3rem calc(var(--widget-content-horizontal-padding) + 1px);
|
||||
position: relative;
|
||||
margin-top: 2rem;
|
||||
animation: errorMessageEntrance 0.4s backwards cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
@keyframes errorMessageEntrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.login-error-message:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-error-message::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--color-negative);
|
||||
opacity: 0.05;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.footer {
|
||||
animation-delay: .4s;
|
||||
animation-duration: 1s;
|
||||
}
|
||||
|
||||
.toggle-password-visibility {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -49,9 +49,10 @@
|
||||
}
|
||||
|
||||
.mobile-navigation-actions > * {
|
||||
padding-block: .9rem;
|
||||
padding-block: 1.1rem;
|
||||
padding-inline: var(--content-bounds-padding);
|
||||
cursor: pointer;
|
||||
transition: background-color 50ms;
|
||||
}
|
||||
|
||||
.mobile-navigation-actions > *:active {
|
||||
|
||||
@@ -153,7 +153,9 @@ body {
|
||||
|
||||
@keyframes loadingContainerEntrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
/* Using 0.001 instead of 0 fixes a random 1s freeze on Chrome on page load when all */
|
||||
/* elements have opacity 0 and are animated in. I don't want to be a web dev anymore. */
|
||||
opacity: 0.001;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +299,17 @@ kbd:active {
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
stroke: var(--color-text-subdue);
|
||||
transition: stroke .2s;
|
||||
}
|
||||
|
||||
.logout-button:hover, .logout-button:focus {
|
||||
stroke: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.theme-choices {
|
||||
--presets-per-row: 2;
|
||||
display: grid;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { find } from "./templating.js";
|
||||
|
||||
const AUTH_ENDPOINT = pageData.baseURL + "/api/authenticate";
|
||||
|
||||
const showPasswordSVG = `<svg class="form-input-icon" stroke="var(--color-text-base)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
</svg>`;
|
||||
|
||||
const hidePasswordSVG = `<svg class="form-input-icon" stroke="var(--color-text-base)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>`;
|
||||
|
||||
const container = find("#login-container");
|
||||
const usernameInput = find("#username");
|
||||
const passwordInput = find("#password");
|
||||
const errorMessage = find("#error-message");
|
||||
const loginButton = find("#login-button");
|
||||
const toggleVisibilityButton = find("#toggle-password-visibility");
|
||||
|
||||
const state = {
|
||||
lastUsername: "",
|
||||
lastPassword: "",
|
||||
isLoading: false,
|
||||
isRateLimited: false
|
||||
};
|
||||
|
||||
const lang = {
|
||||
showPassword: "Show password",
|
||||
hidePassword: "Hide password",
|
||||
incorrectCredentials: "Incorrect username or password",
|
||||
rateLimited: "Too many login attempts, try again in a few minutes",
|
||||
unknownError: "An error occurred, please try again",
|
||||
};
|
||||
|
||||
container.clearStyles("display");
|
||||
setTimeout(() => usernameInput.focus(), 200);
|
||||
|
||||
toggleVisibilityButton
|
||||
.html(showPasswordSVG)
|
||||
.attr("title", lang.showPassword)
|
||||
.on("click", function() {
|
||||
if (passwordInput.type === "password") {
|
||||
passwordInput.type = "text";
|
||||
toggleVisibilityButton.html(hidePasswordSVG).attr("title", lang.hidePassword);
|
||||
return;
|
||||
}
|
||||
|
||||
passwordInput.type = "password";
|
||||
toggleVisibilityButton.html(showPasswordSVG).attr("title", lang.showPassword);
|
||||
});
|
||||
|
||||
function enableLoginButtonIfCriteriaMet() {
|
||||
const usernameValue = usernameInput.value.trim();
|
||||
const passwordValue = passwordInput.value.trim();
|
||||
|
||||
const usernameValid = usernameValue.length >= 3;
|
||||
const passwordValid = passwordValue.length >= 6;
|
||||
|
||||
const isUsingLastCredentials =
|
||||
usernameValue === state.lastUsername
|
||||
&& passwordValue === state.lastPassword;
|
||||
|
||||
loginButton.disabled = !(
|
||||
usernameValid
|
||||
&& passwordValid
|
||||
&& !isUsingLastCredentials
|
||||
&& !state.isLoading
|
||||
&& !state.isRateLimited
|
||||
);
|
||||
}
|
||||
|
||||
usernameInput.on("input", enableLoginButtonIfCriteriaMet);
|
||||
passwordInput.on("input", enableLoginButtonIfCriteriaMet);
|
||||
|
||||
async function handleLoginAttempt() {
|
||||
state.lastUsername = usernameInput.value;
|
||||
state.lastPassword = passwordInput.value;
|
||||
errorMessage.text("");
|
||||
|
||||
loginButton.disable();
|
||||
state.isLoading = true;
|
||||
|
||||
const response = await fetch(AUTH_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: usernameInput.value,
|
||||
password: passwordInput.value
|
||||
}),
|
||||
});
|
||||
|
||||
state.isLoading = false;
|
||||
if (response.status === 200) {
|
||||
container.animate({
|
||||
keyframes: [{ offset: 1, transform: "scale(0.95)", opacity: 0 }],
|
||||
options: { duration: 300, easing: "ease", fill: "forwards" }}
|
||||
);
|
||||
|
||||
find("footer")?.animate({
|
||||
keyframes: [{ offset: 1, opacity: 0 }],
|
||||
options: { duration: 300, easing: "ease", fill: "forwards", delay: 50 }
|
||||
});
|
||||
|
||||
setTimeout(() => { window.location.href = pageData.baseURL + "/"; }, 300);
|
||||
} else if (response.status === 401) {
|
||||
errorMessage.text(lang.incorrectCredentials);
|
||||
passwordInput.focus();
|
||||
} else if (response.status === 429) {
|
||||
errorMessage.text(lang.rateLimited);
|
||||
state.isRateLimited = true;
|
||||
const retryAfter = response.headers.get("Retry-After") || 30;
|
||||
setTimeout(() => {
|
||||
state.lastUsername = "";
|
||||
state.lastPassword = "";
|
||||
state.isRateLimited = false;
|
||||
|
||||
enableLoginButtonIfCriteriaMet();
|
||||
}, retryAfter * 1000);
|
||||
} else {
|
||||
errorMessage.text(lang.unknownError);
|
||||
passwordInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
loginButton.disable().on("click", handleLoginAttempt);
|
||||
@@ -147,6 +147,22 @@ ep.styles = function(s) {
|
||||
return this;
|
||||
}
|
||||
|
||||
ep.clearStyles = function(...props) {
|
||||
for (let i = 0; i < props.length; i++)
|
||||
this.style.removeProperty(props[i]);
|
||||
return this;
|
||||
}
|
||||
|
||||
ep.disable = function() {
|
||||
this.disabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
ep.enable = function() {
|
||||
this.disabled = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
const epAnimate = ep.animate;
|
||||
ep.animate = function(anim, callback) {
|
||||
const a = epAnimate.call(this, anim.keyframes, anim.options);
|
||||
|
||||
Reference in New Issue
Block a user