wcz-layout
Version:
283 lines (282 loc) • 10.1 kB
JavaScript
import { queryOptions } from "@tanstack/react-query";
import { manifest, permissions } from "virtual:wcz-layout";
import { z as z$1 } from "zod";
import { redirect } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { createEnv } from "@t3-oss/env-core";
//#region src/lib/auth/offline.ts
/**
* Offline fallback for identity. The refresh token never leaves the sealed
* httpOnly cookie — what is cached here is only the display snapshot (user
* claims and avatar) so the UI and offline route permissions can still work
* while the server is unreachable. Server requests remain independently
* authorized. At-rest protection is the device's OS login and disk encryption.
*/
const OFFLINE_USER_KEY = "auth-user";
const OFFLINE_PHOTO_KEY = "auth-photo";
/** One shift — matches Intune's default offline grace period. */
const GRACE_PERIOD = 432e5;
const recall = (key) => {
if (typeof localStorage === "undefined") return null;
try {
const raw = localStorage.getItem(key);
if (!raw) return null;
const { value, savedAt } = JSON.parse(raw);
if (Date.now() - savedAt > GRACE_PERIOD) {
localStorage.removeItem(key);
return null;
}
return value;
} catch {
localStorage.removeItem(key);
return null;
}
};
const remember = (key, value) => {
if (typeof localStorage === "undefined") return;
try {
if (value === null || value === void 0) localStorage.removeItem(key);
else localStorage.setItem(key, JSON.stringify({
value,
savedAt: Date.now()
}));
} catch {}
};
const isOfflineFailure = (error) => typeof navigator !== "undefined" && !navigator.onLine || error instanceof TypeError;
/**
* Answers from the server when reachable, remembering the result; when the call
* has a transport failure, replays the last result instead, for up to
* {@link GRACE_PERIOD} since it was last confirmed.
*
* Cached values are untrusted client state. Server authorization must never
* depend on them.
*/
const withOfflineFallback = async (key, fetchFresh) => {
try {
const fresh = await fetchFresh();
remember(key, fresh);
return fresh;
} catch (error) {
if (!isOfflineFailure(error)) throw error;
return recall(key);
}
};
/** Drops the cached identity. Call on logout, before leaving the page. */
const clearSessionSnapshot = () => {
if (typeof localStorage === "undefined") return;
localStorage.removeItem(OFFLINE_USER_KEY);
localStorage.removeItem(OFFLINE_PHOTO_KEY);
localStorage.setItem("auth-logout", String(Date.now()));
};
if (typeof window !== "undefined") window.addEventListener("storage", (event) => {
if (event.key === "auth-logout") location.href = "/auth/logout";
});
createEnv({
clientPrefix: "VITE_",
client: {},
runtimeEnv: import.meta.env,
emptyStringAsUndefined: true
});
const serverEnv = createEnv({
server: {
ENTRA_CLIENT_ID: z$1.string().min(1),
ENTRA_TENANT_ID: z$1.string().min(1),
ENTRA_CLIENT_SECRET: z$1.string().min(1),
AWS_ISSUER: z$1.url().optional(),
AWS_DOMAIN: z$1.url().optional(),
AWS_CLIENT_ID: z$1.string().min(1).optional(),
AWS_CLIENT_SECRET: z$1.string().min(1).optional(),
SESSION_SECRET: z$1.string().min(32),
FILE_BASE_URL: z$1.string().min(1).optional(),
APPROVAL_BASE_URL: z$1.string().min(1).optional(),
PEOPLESOFT_BASE_URL: z$1.string().min(1).optional(),
EMAIL_BASE_URL: z$1.string().min(1).optional(),
AI_BASE_URL: z$1.string().min(1).optional(),
AI_SUBSCRIPTION_KEY: z$1.string().min(1).optional()
},
runtimeEnv: process.env,
emptyStringAsUndefined: true
});
//#endregion
//#region src/lib/auth/session.ts
const getAppSession = async () => {
const { useSession: getSession } = await import("@tanstack/react-start/server");
return getSession({
name: "session",
password: serverEnv.SESSION_SECRET
});
};
//#endregion
//#region src/lib/auth/user.ts
/**
* Reads the signed-in user from the session cookie, or null. As a server function
* it runs in-process when called on the server (SSR, middleware) and as an RPC
* when called from the client — so it doubles as the client `queryFn`.
*/
const getSessionUser = createServerFn({ method: "GET" }).handler(async () => {
return (await getAppSession()).data.user ?? null;
});
/** The identity providers the server offers, for the login page. */
const getLoginProviders = createServerFn({ method: "GET" }).handler(async () => {
const { providers } = await import("./providers-Dbl95AxR.mjs").then((n) => n.i);
return providers.map(({ id, label }) => ({
id,
label
}));
});
/**
* Feeds `LoginForm`. Server config, not per-user, so it is safe to prime on the
* server — the `/login` route's loader does that to avoid a flash of empty card.
*/
const loginProvidersQueryOptions = queryOptions({
queryKey: ["auth", "providers"],
queryFn: () => getLoginProviders(),
staleTime: "static"
});
/**
* Client-side query for the signed-in user. Shared between `getUser` (route
* `beforeLoad`) and the always-mounted observer in `LayoutProvider` — the
* observer is what seeds the offline snapshot on the very first page load
* (`beforeLoad` does not re-run after SSR hydration) and what makes
* `refetchOnReconnect` fire once connectivity returns.
*/
const userQueryOptions = queryOptions({
queryKey: ["auth", "user"],
queryFn: () => withOfflineFallback(OFFLINE_USER_KEY, () => getSessionUser()),
staleTime: Infinity,
networkMode: "offlineFirst",
refetchOnReconnect: "always"
});
const getUser = ({ queryClient }) => {
if (import.meta.env.SSR) return getSessionUser();
return queryClient.ensureQueryData(userQueryOptions);
};
//#endregion
//#region src/lib/utils.ts
const WISTRON_PRIMARY_COLOR = "#00506E";
const WISTRON_SECONDARY_COLOR = "#64DC00";
var Platform = class {
static get isAndroid() {
return /android/i.test(this.userAgent);
}
static get isIOS() {
return /iPad|iPhone|iPod/.test(this.userAgent);
}
static get isWindows() {
return /windows/i.test(this.userAgent);
}
static get isMacOS() {
return /Macintosh|MacIntel|MacPPC|Mac68K/.test(this.userAgent);
}
static get userAgent() {
return typeof navigator === "undefined" ? "" : navigator.userAgent;
}
};
const rootRouteHead = (options) => ({
meta: [
{ charSet: "utf-8" },
{
name: "viewport",
content: "width=device-width, initial-scale=1"
},
{ title: manifest.name },
{
name: "og:type",
content: "website"
},
{
name: "og:title",
content: manifest.name
},
{
name: "og:image",
content: "/favicon-32x32.png"
}
],
links: [
{
rel: "apple-touch-icon",
sizes: "180x180",
href: "/apple-touch-icon.png"
},
{
rel: "icon",
type: "image/png",
sizes: "32x32",
href: "/favicon-32x32.png"
},
{
rel: "icon",
type: "image/png",
sizes: "16x16",
href: "/favicon-16x16.png"
},
{
rel: "manifest",
href: options?.manifest || "/manifest.json"
},
{
rel: "icon",
href: "/favicon.ico"
}
]
});
/**
* Route guard for `beforeLoad`. Routes are public by default; add this to close one.
*
* - `requireAuth()` — signed in with any provider. The only gate a population
* without permission groups (e.g. AWS users) can pass.
* - `requireAuth("admin")` — signed in *and* in a group listed under that key.
*
* `reloadDocument` is required: `/auth/login` is a component-less server route and
* must be reached with a full-document navigation. It starts the login directly when
* only one provider is registered, otherwise it forwards to the `/login` chooser.
*/
const requireAuth = (permissionKey) => {
return async ({ location, context }) => {
const user = await getUser({ queryClient: context.queryClient });
if (!user) throw redirect({
href: `/auth/login?returnTo=${encodeURIComponent(location.href)}`,
reloadDocument: true
});
if (permissionKey && !hasPermission(user, permissionKey)) throw new Error("You do not have permission to access this page.");
return { user };
};
};
const NON_PRODUCTION = /(?:^|[.-])(?:dev|qas|uat|test)(?:[.-]|$)/i;
/** True only for a dev/qas/uat/test domain — localhost and production are not. */
const isTestEnv = (origin) => NON_PRODUCTION.test((origin ?? "").split(".").slice(1).join("."));
const getFieldStatus = (field) => {
const { meta } = field.state;
return {
isTouched: meta.isTouched,
hasError: !!meta.errors.length,
helperText: meta.errors[0]?.message
};
};
/**
* Only same-origin paths are allowed, so `returnTo` cannot become an open
* redirect. Backslash counts as a separator too: browsers normalize `/\evil`
* to the protocol-relative `//evil`.
*/
const sanitizeReturnTo = (value) => value && /^\/(?![/\\])/.test(value) ? value : "/";
const foldForSearch = (value) => String(value ?? "").toLowerCase().normalize("NFD").replaceAll(/[\u{300}-\u{36F}]/gu, "").normalize("NFC");
const permissionGroups = new Set(Object.values(permissions).flat());
/**
* Maps id/access-token claims to the serializable `User`. Groups are trimmed to
* those named in `permissions.ts` — an unfiltered group list would blow the
* cookie. Cognito's claim names differ, so both are read.
*/
const buildUser = (payload, provider) => ({
provider,
name: (payload.name ?? payload["cognito:username"] ?? "").split("/")[0],
email: (payload.preferred_username ?? payload.email ?? "").toLowerCase(),
department: payload.department?.toUpperCase() || "",
employeeId: (payload.employeeId ?? payload["cognito:username"] ?? "").toUpperCase(),
companyName: payload.companyName || "",
groups: (payload.groups ?? payload["cognito:groups"])?.filter((group) => permissionGroups.has(group)) ?? []
});
const hasPermission = (user, key) => user ? permissions[key].some((group) => user.groups.includes(group)) : false;
//#endregion
export { serverEnv as _, foldForSearch as a, withOfflineFallback as b, isTestEnv as c, sanitizeReturnTo as d, getSessionUser as f, getAppSession as g, userQueryOptions as h, buildUser as i, requireAuth as l, loginProvidersQueryOptions as m, WISTRON_PRIMARY_COLOR as n, getFieldStatus as o, getUser as p, WISTRON_SECONDARY_COLOR as r, hasPermission as s, Platform as t, rootRouteHead as u, OFFLINE_PHOTO_KEY as v, clearSessionSnapshot as y };
//# sourceMappingURL=utils-KaM5VMTJ.mjs.map