analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 10.6 kB
Source Map (JSON)
{"version":3,"sources":["../src/utils/domainUtils.ts"],"sourcesContent":["/**\n * Resolves the root hostname shared by all application subdomains.\n * Mirrors the subdomain rules used by `getRootDomain` (Auth), without\n * protocol or port. Useful for the cookie `Domain` attribute so that a\n * cookie set on a subdomain is visible on the root domain and vice versa.\n *\n * @param hostname - The hostname to resolve (e.g. window.location.hostname)\n * @returns The root hostname, or null for localhost / IP literals\n * (callers should omit the cookie Domain attribute in that case)\n *\n * @example\n * ```typescript\n * resolveRootHostname('aluno.analyticaensino.com.br'); // 'analyticaensino.com.br'\n * resolveRootHostname('aluno.hml.analyticaensino.com.br'); // 'hml.analyticaensino.com.br'\n * resolveRootHostname('sub.example.com'); // 'example.com'\n * resolveRootHostname('localhost'); // null\n * resolveRootHostname('127.0.0.1'); // null\n * ```\n */\nexport const resolveRootHostname = (hostname: string): string | null => {\n if (hostname === 'localhost') {\n return null;\n }\n\n // IP literals: no subdomain logic applies\n const isIPv4 = /^\\d{1,3}(?:\\.\\d{1,3}){3}$/.test(hostname);\n const isIPv6 = hostname.includes(':'); // simple check is sufficient here\n if (isIPv4 || isIPv6) {\n return null;\n }\n\n const parts = hostname.split('.');\n // Label-based detection: matches 'hml' and 'hml-*' labels (e.g. hml-aluno)\n // without false positives on substrings like 'html'\n const isHml = parts.some(\n (label) => label === 'hml' || label.startsWith('hml-')\n );\n\n // Handle Brazilian .com.br domains and similar patterns.\n // Use index access rather than Array.prototype.at(): `.at()` is unsupported\n // on older mobile browsers (e.g. Chrome < 92 on Android 7) and threw\n // \"TypeError: r.at is not a function\" here during theme init on app mount,\n // breaking the page for those users (FRONTEND-LOGIN-WEB-20).\n if (\n parts.length >= 3 &&\n parts[parts.length - 2] === 'com' &&\n parts[parts.length - 1] === 'br'\n ) {\n if (parts.length === 3) {\n // Already at root level for .com.br (e.g., analyticaensino.com.br)\n return hostname;\n }\n // For domains like aluno.analyticaensino.com.br, return analyticaensino.com.br\n const base = parts.slice(-3).join('.');\n // If in hml environment, resolve to hml.base (e.g., hml.analyticaensino.com.br)\n return isHml && !base.startsWith('hml.') ? `hml.${base}` : base;\n }\n\n // Only treat as subdomain if there are 3+ parts (e.g., subdomain.example.com)\n if (parts.length > 2) {\n const base = parts.slice(-2).join('.');\n return isHml && !base.startsWith('hml.') ? `hml.${base}` : base;\n }\n\n // For 2-part domains (example.com) or single domains, return as-is\n return hostname;\n};\n\n/**\n * Extracts the profile \"slug\" from a hostname's leading subdomain label,\n * stripping the homolog prefix so it matches the bare profile name the backend\n * returns (e.g. \"aluno\"). Used to compare the profile a shared link belongs to\n * against the profile the user actually logged in as.\n *\n * @param hostname - The hostname to read (e.g. window.location.hostname)\n * @returns The profile slug, or null for localhost / IP literals / hosts with\n * no subdomain (a root-level host carries no profile identity)\n *\n * @example\n * ```typescript\n * extractSubdomainSlug('aluno.analyticaensino.com.br'); // 'aluno'\n * extractSubdomainSlug('hml-aluno.hml.analyticaensino.com.br'); // 'aluno'\n * extractSubdomainSlug('analyticaensino.com.br'); // null (no subdomain)\n * extractSubdomainSlug('localhost'); // null\n * ```\n */\nexport const extractSubdomainSlug = (hostname: string): string | null => {\n // localhost / IP literals have no profile subdomain\n if (resolveRootHostname(hostname) === null) {\n return null;\n }\n\n // If stripping the subdomain yields the hostname unchanged, there is no\n // leading profile label (already at root, e.g. analyticaensino.com.br).\n if (resolveRootHostname(hostname) === hostname) {\n return null;\n }\n\n const firstLabel = hostname.split('.')[0];\n // Homolog uses a \"hml-<profile>\" leading label; strip it to the bare profile.\n return firstLabel.startsWith('hml-')\n ? firstLabel.slice('hml-'.length)\n : firstLabel;\n};\n\n/**\n * Builds the login URL (root domain) with the current deep link preserved as a\n * `returnTo` query param, so that after the user logs in they can be sent back\n * to the page they originally tried to open.\n *\n * Deliberately a no-op in two cases, returning the bare `rootDomain`:\n * - **localhost / IP literals**: the deep-link-return flow is skipped entirely\n * in local development (no subdomain to validate against).\n * - **no meaningful path**: the user was at the root already, so there is\n * nothing worth remembering.\n *\n * The stored value is the full current URL (origin + path + query). The login\n * app later validates that its host's profile matches the logged-in profile\n * before honoring it, and only ever reuses the path — never the host — so a\n * crafted `returnTo` cannot redirect to a foreign origin.\n *\n * Also a no-op right after an explicit logout (see `markExplicitLogout`): a\n * user who chose to sign out should never be bounced back to the page they were\n * on, and clearing the session synchronously can make ProtectedRoute redirect\n * through here before the app's own clean logout navigation completes.\n *\n * @param rootDomain - The login/root domain to redirect to (from getRootDomain)\n * @returns `rootDomain` with `?returnTo=<encoded current URL>` appended, or the\n * bare `rootDomain` when the flow should be skipped\n */\nexport const buildLoginUrlWithReturnTo = (rootDomain: string): string => {\n if (typeof window === 'undefined') {\n return rootDomain;\n }\n\n // An explicit logout must not preserve a deep link.\n if (isExplicitLogoutActive()) {\n return rootDomain;\n }\n\n const { hostname, pathname, search } = window.location;\n\n // Skip the whole deep-link flow on localhost / IP literals.\n if (resolveRootHostname(hostname) === null) {\n return rootDomain;\n }\n\n // Nothing worth remembering when already at the root with no query.\n if (pathname === '/' && !search) {\n return rootDomain;\n }\n\n const returnTo = encodeURIComponent(window.location.href);\n return `${rootDomain}?returnTo=${returnTo}`;\n};\n\nconst EXPLICIT_LOGOUT_KEY = '@auth:explicit-logout';\n// How long after markExplicitLogout() a redirect is still treated as part of\n// the logout. Long enough to cover the redirects a single logout fans out\n// (ProtectedRoute re-render + any in-flight request that 401s as the tokens are\n// cleared), short enough that a genuine session expiry later is unaffected.\nconst EXPLICIT_LOGOUT_WINDOW_MS = 5000;\n\n/**\n * Marks that the user is intentionally logging out, so redirects to login\n * during the logout do NOT preserve a `returnTo` deep link. Call this right\n * before clearing the session in an explicit \"Sair\" handler.\n *\n * Stored as a timestamp (not a one-shot flag) because a single logout can\n * trigger MORE THAN ONE redirect through buildLoginUrlWithReturnTo — e.g. the\n * ProtectedRoute re-render after signOut() AND the 401 interceptor firing on an\n * in-flight request whose token was just cleared. A one-shot flag would be\n * eaten by the first redirect, letting the second re-append returnTo. A\n * time-boxed marker covers every redirect in the logout window and then expires\n * on its own, so it never suppresses a later, genuine session-expiry redirect.\n */\nexport const markExplicitLogout = (): void => {\n if (typeof window === 'undefined') return;\n try {\n sessionStorage.setItem(EXPLICIT_LOGOUT_KEY, String(Date.now()));\n } catch {\n // sessionStorage unavailable: worst case the URL carries a harmless\n // returnTo that the login flow validates and can still ignore.\n }\n};\n\n/**\n * Returns true when an explicit logout was marked within the recent window.\n * Does not clear the marker (it expires by time), so several redirects fanned\n * out by the same logout all see it.\n */\nconst isExplicitLogoutActive = (): boolean => {\n if (typeof window === 'undefined') return false;\n try {\n const raw = sessionStorage.getItem(EXPLICIT_LOGOUT_KEY);\n if (!raw) return false;\n const ts = Number(raw);\n if (!Number.isFinite(ts)) return false;\n const active = Date.now() - ts < EXPLICIT_LOGOUT_WINDOW_MS;\n // Once expired, remove it so it can't linger and suppress a future 401.\n if (!active) sessionStorage.removeItem(EXPLICIT_LOGOUT_KEY);\n return active;\n } catch {\n return false;\n }\n};\n"],"mappings":";AAmBO,IAAM,sBAAsB,CAAC,aAAoC;AACtE,MAAI,aAAa,aAAa;AAC5B,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,4BAA4B,KAAK,QAAQ;AACxD,QAAM,SAAS,SAAS,SAAS,GAAG;AACpC,MAAI,UAAU,QAAQ;AACpB,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,SAAS,MAAM,GAAG;AAGhC,QAAM,QAAQ,MAAM;AAAA,IAClB,CAAC,UAAU,UAAU,SAAS,MAAM,WAAW,MAAM;AAAA,EACvD;AAOA,MACE,MAAM,UAAU,KAChB,MAAM,MAAM,SAAS,CAAC,MAAM,SAC5B,MAAM,MAAM,SAAS,CAAC,MAAM,MAC5B;AACA,QAAI,MAAM,WAAW,GAAG;AAEtB,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,MAAM,EAAE,EAAE,KAAK,GAAG;AAErC,WAAO,SAAS,CAAC,KAAK,WAAW,MAAM,IAAI,OAAO,IAAI,KAAK;AAAA,EAC7D;AAGA,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,OAAO,MAAM,MAAM,EAAE,EAAE,KAAK,GAAG;AACrC,WAAO,SAAS,CAAC,KAAK,WAAW,MAAM,IAAI,OAAO,IAAI,KAAK;AAAA,EAC7D;AAGA,SAAO;AACT;AAoBO,IAAM,uBAAuB,CAAC,aAAoC;AAEvE,MAAI,oBAAoB,QAAQ,MAAM,MAAM;AAC1C,WAAO;AAAA,EACT;AAIA,MAAI,oBAAoB,QAAQ,MAAM,UAAU;AAC9C,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,SAAS,MAAM,GAAG,EAAE,CAAC;AAExC,SAAO,WAAW,WAAW,MAAM,IAC/B,WAAW,MAAM,OAAO,MAAM,IAC9B;AACN;AA2BO,IAAM,4BAA4B,CAAC,eAA+B;AACvE,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAGA,MAAI,uBAAuB,GAAG;AAC5B,WAAO;AAAA,EACT;AAEA,QAAM,EAAE,UAAU,UAAU,OAAO,IAAI,OAAO;AAG9C,MAAI,oBAAoB,QAAQ,MAAM,MAAM;AAC1C,WAAO;AAAA,EACT;AAGA,MAAI,aAAa,OAAO,CAAC,QAAQ;AAC/B,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,mBAAmB,OAAO,SAAS,IAAI;AACxD,SAAO,GAAG,UAAU,aAAa,QAAQ;AAC3C;AAEA,IAAM,sBAAsB;AAK5B,IAAM,4BAA4B;AAe3B,IAAM,qBAAqB,MAAY;AAC5C,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,mBAAe,QAAQ,qBAAqB,OAAO,KAAK,IAAI,CAAC,CAAC;AAAA,EAChE,QAAQ;AAAA,EAGR;AACF;AAOA,IAAM,yBAAyB,MAAe;AAC5C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,UAAM,MAAM,eAAe,QAAQ,mBAAmB;AACtD,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,KAAK,OAAO,GAAG;AACrB,QAAI,CAAC,OAAO,SAAS,EAAE,EAAG,QAAO;AACjC,UAAM,SAAS,KAAK,IAAI,IAAI,KAAK;AAEjC,QAAI,CAAC,OAAQ,gBAAe,WAAW,mBAAmB;AAC1D,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}