UNPKG

@vaadin/hilla-frontend

Version:

Hilla core frontend utils

208 lines 7.03 kB
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