@vaadin/hilla-frontend
Version:
Hilla core frontend utils
208 lines • 7.03 kB
JavaScript
import CookieManager from "./CookieManager.js";
import csrfInfoSource, { VAADIN_CSRF_HEADER, clearCsrfInfoMeta, CsrfInfoType, extractCsrfInfoFromMeta, updateCsrfInfoMeta } from "./CsrfInfoSource.js";
function createHeaders(headerEntries) {
const headers = new Headers();
for (const [name, value] of headerEntries) {
headers.append(name, value);
}
return headers;
}
const JWT_COOKIE_NAME = "jwt.headerAndPayload";
async function getCsrfInfoFromResponseBody(body) {
const doc = new DOMParser().parseFromString(body, "text/html");
return extractCsrfInfoFromMeta(doc);
}
async function updateCsrfTokensBasedOnResponse(response) {
const responseText = await response.text();
const csrfInfo = await getCsrfInfoFromResponseBody(responseText);
updateCsrfInfoMeta(csrfInfo, document);
}
async function doFetchLogout(logoutUrl, headerEntries) {
const headers = createHeaders(headerEntries);
const response = await fetch(logoutUrl, {
headers,
method: "POST"
});
if (!response.ok) {
throw new Error(`failed to logout with response ${response.status}`);
}
await updateCsrfTokensBasedOnResponse(response);
csrfInfoSource.reset();
return response;
}
async function doFormLogout(url, formDataEntries) {
const logoutUrl = typeof url === "string" ? url : url.toString();
const form = document.createElement("form");
form.setAttribute("method", "POST");
form.setAttribute("action", logoutUrl);
form.style.display = "none";
for (const [name, value] of formDataEntries) {
const input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", name);
input.setAttribute("value", value);
form.appendChild(input);
}
document.body.appendChild(form);
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error("Form submission did not navigate away after 10 seconds."));
}, 1e4);
form.submit();
});
}
async function doLogout(doc, options) {
const shouldSubmitFormLogout = !options?.navigate && !options?.onSuccess;
const logoutUrl = options?.logoutUrl ?? "logout";
const csrfInfo = doc === document ? await csrfInfoSource.get() : await extractCsrfInfoFromMeta(doc);
if (shouldSubmitFormLogout) {
const formDataEntries = csrfInfo.type === CsrfInfoType.SPRING ? csrfInfo.formDataEntries : [];
await doFormLogout(logoutUrl, formDataEntries);
return new Response(null, {
status: 500,
statusText: "Form submission did not navigate away."
});
}
const headerEntries = csrfInfo.type === CsrfInfoType.SPRING ? csrfInfo.headerEntries : [];
return await doFetchLogout(logoutUrl, headerEntries);
}
function normalizePath(url) {
const effectiveBaseURL = new URL(".", document.baseURI);
const effectiveBaseURI = effectiveBaseURL.toString();
let normalized = url;
if (normalized.startsWith(effectiveBaseURL.pathname)) {
return `/${normalized.slice(effectiveBaseURL.pathname.length)}`;
}
normalized = normalized.startsWith(effectiveBaseURI) ? `/${normalized.slice(effectiveBaseURI.length)}` : normalized;
return normalized;
}
/**
* Navigates to the provided path using page reload.
*
* @param to - navigation target path
*/
function navigateWithPageReload(to) {
const url = to.startsWith("/") ? new URL(`.${to}`, document.baseURI) : to;
window.location.replace(url);
}
/**
* A helper method for Spring Security based form login.
* @param username - username
* @param password - password
* @param options - defines additional options, e.g, the loginProcessingUrl etc.
*/
export async function login(username, password, options) {
try {
const data = new FormData();
data.append("username", username);
data.append("password", password);
const loginProcessingUrl = options?.loginProcessingUrl ?? "login";
const csrfInfo = await csrfInfoSource.get();
const headers = createHeaders(csrfInfo.headerEntries);
headers.append("source", "typescript");
const response = await fetch(loginProcessingUrl, {
body: data,
headers,
method: "POST"
});
const result = response.headers.get("Result");
const savedUrl = response.headers.get("Saved-url") ?? undefined;
const defaultUrl = response.headers.get("Default-url") ?? undefined;
const loginSuccessful = response.ok && result === "success";
if (loginSuccessful) {
const springCsrfHeader = response.headers.get("Spring-CSRF-header") ?? undefined;
const springCsrfToken = response.headers.get("Spring-CSRF-token") ?? undefined;
if (springCsrfHeader && springCsrfToken) {
updateCsrfInfoMeta({
headerEntries: [[springCsrfHeader, springCsrfToken]],
formDataEntries: [],
type: CsrfInfoType.SPRING,
timestamp: Date.now()
}, document);
csrfInfoSource.reset();
}
if (options?.onSuccess) {
await options.onSuccess();
}
const url = savedUrl ?? defaultUrl ?? document.baseURI;
const toPath = normalizePath(url);
const navigate = options?.navigate ?? navigateWithPageReload;
navigate(toPath);
return {
defaultUrl,
error: false,
redirectUrl: savedUrl
};
}
return {
error: true,
errorMessage: "Check that you have entered the correct username and password and try again.",
errorTitle: "Incorrect username or password."
};
} catch (e) {
if (e instanceof Error) {
return {
error: true,
errorMessage: e.message,
errorTitle: e.name
};
}
throw e;
}
}
/**
* A helper method for Spring Security based form logout
* @param options - defines additional options, e.g, the logoutUrl.
*/
export async function logout(options) {
let response;
try {
response = await doLogout(document, options);
} catch {
try {
const noCacheResponse = await fetch("?nocache");
const responseText = await noCacheResponse.text();
const doc = new DOMParser().parseFromString(responseText, "text/html");
response = await doLogout(doc, options);
} catch (error) {
clearCsrfInfoMeta(document);
csrfInfoSource.reset();
throw error;
}
} finally {
CookieManager.remove(JWT_COOKIE_NAME);
if (response && response.ok && response.redirected) {
if (options?.onSuccess) {
await options.onSuccess();
}
const toPath = normalizePath(response.url);
const navigate = options?.navigate ?? navigateWithPageReload;
navigate(toPath);
}
}
}
/**
* A helper class for handling invalid sessions during an endpoint call.
* E.g., you can use this to show user a login page when the session has
* expired.
*/
export class InvalidSessionMiddleware {
onInvalidSessionCallback;
constructor(onInvalidSessionCallback) {
this.onInvalidSessionCallback = onInvalidSessionCallback;
}
async invoke(context, next) {
const clonedContext = { ...context };
clonedContext.request = context.request.clone();
const response = await next(context);
if (response.status === 401) {
const loginResult = await this.onInvalidSessionCallback();
if (loginResult.token) {
clonedContext.request.headers.set(VAADIN_CSRF_HEADER, loginResult.token);
return next(clonedContext);
}
}
return response;
}
}
//# sourceMappingURL=./Authentication.js.map