Publica versión definitiva de Seguimiento de Impuestos GLM

This commit is contained in:
2026-07-31 15:24:26 -04:00
parent 2fad73040e
commit 9b75955a5b
26 changed files with 318 additions and 44 deletions
+2 -28
View File
@@ -1,30 +1,4 @@
# Dependencies
node_modules/
# Build output
dist/
# Local environment
.env
.env.*
!.env.example
*.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editors and operating system
.vscode/*
!.vscode/extensions.json
.idea/
.DS_Store
Thumbs.db
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env.local
.env.production
+1 -1
View File
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
File diff suppressed because one or more lines are too long
+44 -1
View File
@@ -10,6 +10,49 @@
/>
<meta name="author" content="GomezLee Marketing" />
<meta name="theme-color" content="#4F758B" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<script>
(() => {
const navigationKey = "glm-tax-oauth-navigation-v3";
const refreshKey = "__glm_auth_fresh";
const hasOAuthResult = () => {
const hash = new URLSearchParams(window.location.hash.replace(/^#/, ""));
return (
hash.has("access_token") ||
hash.has("refresh_token") ||
hash.has("error") ||
hash.has("error_description")
);
};
const forceFreshDocument = () => {
let pending = false;
try {
pending = Boolean(sessionStorage.getItem(navigationKey));
} catch {
pending = false;
}
if (!pending || !hasOAuthResult()) return;
const url = new URL(window.location.href);
if (url.searchParams.has(refreshKey)) return;
// Este parámetro se agrega DESPUÉS de volver de Supabase. Por eso no
// forma parte de redirect_to ni requiere una URL nueva en EasyPanel.
url.searchParams.set(refreshKey, String(Date.now()));
window.location.replace(url.toString());
};
forceFreshDocument();
window.addEventListener("pageshow", (event) => {
if (event.persisted) forceFreshDocument();
});
})();
</script>
<meta property="og:title" content="Seguimiento de Impuestos GLM" />
<meta
property="og:description"
@@ -21,7 +64,7 @@
<link rel="icon" type="image/png" sizes="192x192" href="/calendario-impuestos/favicon-192.png?v=glm-2026" />
<link rel="shortcut icon" href="/calendario-impuestos/favicon.ico?v=glm-2026" />
<link rel="apple-touch-icon" sizes="180x180" href="/calendario-impuestos/apple-touch-icon.png?v=glm-2026" />
<script type="module" crossorigin src="/calendario-impuestos/assets/index-3IoCMmqv.js"></script>
<script type="module" crossorigin src="/calendario-impuestos/assets/index-BsCJFT61.js"></script>
<link rel="stylesheet" crossorigin href="/calendario-impuestos/assets/index-DrJGHNJG.css">
</head>
<body>
+43
View File
@@ -10,6 +10,49 @@
/>
<meta name="author" content="GomezLee Marketing" />
<meta name="theme-color" content="#4F758B" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<script>
(() => {
const navigationKey = "glm-tax-oauth-navigation-v3";
const refreshKey = "__glm_auth_fresh";
const hasOAuthResult = () => {
const hash = new URLSearchParams(window.location.hash.replace(/^#/, ""));
return (
hash.has("access_token") ||
hash.has("refresh_token") ||
hash.has("error") ||
hash.has("error_description")
);
};
const forceFreshDocument = () => {
let pending = false;
try {
pending = Boolean(sessionStorage.getItem(navigationKey));
} catch {
pending = false;
}
if (!pending || !hasOAuthResult()) return;
const url = new URL(window.location.href);
if (url.searchParams.has(refreshKey)) return;
// Este parámetro se agrega DESPUÉS de volver de Supabase. Por eso no
// forma parte de redirect_to ni requiere una URL nueva en EasyPanel.
url.searchParams.set(refreshKey, String(Date.now()));
window.location.replace(url.toString());
};
forceFreshDocument();
window.addEventListener("pageshow", (event) => {
if (event.persisted) forceFreshDocument();
});
})();
</script>
<meta property="og:title" content="Seguimiento de Impuestos GLM" />
<meta
property="og:description"
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"build": "tsc --noEmit && vite build && node scripts/create-legacy-asset-aliases.mjs",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
+49
View File
@@ -0,0 +1,49 @@
import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
import { basename, join } from "node:path";
import { fileURLToPath } from "node:url";
const assetsDir = new URL("../dist/assets/", import.meta.url);
const assetsPath = fileURLToPath(assetsDir);
if (!existsSync(assetsPath)) process.exit(0);
mkdirSync(assetsPath, { recursive: true });
const files = readdirSync(assetsPath);
const currentJs = files.find((name) => /^index-[A-Za-z0-9_-]+\.js$/.test(name));
const currentCss = files.find((name) => /^index-[A-Za-z0-9_-]+\.css$/.test(name));
const legacyJs = [
"index-3IoCMmqv.js",
"index-DjmlsRNV.js",
"index-7Fzi8kyG.js",
"index-OAuthRedirectExacto.js",
"index-oauthv3-ed1f63a671.js",
"index-C1g15Gzk.js",
"index-DvnNNw_U.js",
"index-B7i36ZCk.js",
"index-DcLwYapC.js",
"index-k1Oz2sTE.js",
"index-rurDav1q.js",
];
const legacyCss = [
"index-DrJGHNJG.css",
"index-CD8ZsNgz.css",
"index-_4eSo35_.css",
"index-CvdK_cH7.css",
"index-DPuohwR6.css",
"index-DGUCvIOo.css",
"index-C-5g0-Hp.css",
];
function copyAliases(sourceName, aliases) {
if (!sourceName) return;
const source = join(assetsPath, sourceName);
for (const alias of aliases) {
if (alias === basename(source)) continue;
copyFileSync(source, join(assetsPath, alias));
}
}
copyAliases(currentJs, legacyJs);
copyAliases(currentCss, legacyCss);
+4 -2
View File
@@ -1603,7 +1603,8 @@ function ContactManagerModal({
<div className="rounded-xl border border-glm-blue/15 bg-glm-blue-soft/35 p-4">
<p className="text-sm font-semibold text-glm-blue">Búsqueda automática</p>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
Escribe el nombre completo. La app consultará BambooHR y propondrá país, área, cargo y correo. Podrás modificar todo antes de guardar.
Escribe el nombre completo. La app consultará BambooHR y propondrá país, área, cargo
y correo. Podrás modificar todo antes de guardar.
</p>
</div>
<Field label="Nombre completo">
@@ -1814,7 +1815,8 @@ function ContactForm({
const invalid =
!draft.name.trim() ||
!draft.countryId ||
((draft.emailEnabled || draft.googleChatEnabled || draft.calendarEnabled) && !draft.email.trim());
((draft.emailEnabled || draft.googleChatEnabled || draft.calendarEnabled) &&
!draft.email.trim());
return (
<form
+3 -3
View File
@@ -90,10 +90,10 @@ export default function AuthGate() {
}
};
const handleLogin = () => {
const handleLogin = async () => {
setActionPending(true);
try {
signInWithGoogle();
await signInWithGoogle();
} catch (error) {
setActionPending(false);
setState({ status: "signed-out", error: getErrorMessage(error) });
@@ -192,7 +192,7 @@ export default function AuthGate() {
<button
type="button"
onClick={handleLogin}
onClick={() => void handleLogin()}
disabled={actionPending}
className="flex w-full items-center justify-center gap-3 rounded-lg border border-border bg-card px-4 py-3 text-sm font-semibold text-foreground shadow-sm transition hover:-translate-y-px hover:border-glm-blue/35 hover:shadow-md disabled:cursor-wait disabled:opacity-60"
>
+60 -8
View File
@@ -31,6 +31,8 @@ const anonKey = String(import.meta.env.VITE_SUPABASE_ANON_KEY ?? "").trim();
const supabaseKey = publishableKey || anonKey;
const SESSION_STORAGE_KEY = "glm-tax-auth-session-v1";
const OAUTH_NAVIGATION_KEY = "glm-tax-oauth-navigation-v3";
const OAUTH_FRESH_QUERY_KEY = "__glm_auth_fresh";
const EXPIRY_MARGIN_SECONDS = 60;
export const isSupabaseConfigured = Boolean(supabaseUrl && supabaseKey);
@@ -48,22 +50,30 @@ export function getSupabaseConfigurationMessage(): string | null {
}
export function getOAuthRedirectUrl(): string {
// La URL debe coincidir exactamente con una de las permitidas en Supabase.
// No se agregan parámetros variables porque Auth descartaría redirect_to y
// terminaría enviando al SITE_URL de Supabase.
// Se conserva exactamente la misma URL que ya está permitida en Supabase.
// La actualización del documento se hace después del retorno, desde el
// navegador, por lo que no requiere agregar una URL nueva en EasyPanel.
return new URL(import.meta.env.BASE_URL, window.location.origin).toString();
}
export function signInWithGoogle(): void {
export async function signInWithGoogle(): Promise<void> {
assertConfigured();
const redirectUrl = getOAuthRedirectUrl();
markOAuthNavigation();
// Actualiza la entrada HTML en la caché HTTP antes de salir hacia Google.
// Así, el retorno de OAuth no reutiliza un index.html anterior del servidor.
await warmLatestApplicationEntry(redirectUrl);
const params = new URLSearchParams({
provider: "google",
redirect_to: getOAuthRedirectUrl(),
redirect_to: redirectUrl,
prompt: "select_account",
});
window.location.assign(`${supabaseUrl}/auth/v1/authorize?${params.toString()}`);
// replace evita que el navegador restaure la pantalla previa mediante BFCache.
window.location.replace(`${supabaseUrl}/auth/v1/authorize?${params.toString()}`);
}
export async function restoreSession(): Promise<SupabaseAuthSession | null> {
@@ -113,6 +123,7 @@ export async function signOut(session?: SupabaseAuthSession | null): Promise<voi
// El cierre local debe completarse incluso si Supabase no está disponible.
} finally {
clearStoredSession();
clearOAuthNavigation();
}
}
@@ -121,6 +132,7 @@ async function consumeOAuthCallback(): Promise<SupabaseAuthSession | null> {
const oauthError = hash.get("error_description") ?? hash.get("error");
if (oauthError) {
clearOAuthNavigation();
cleanOAuthFragment();
throw new Error(oauthError.replace(/\+/g, " "));
}
@@ -131,7 +143,8 @@ async function consumeOAuthCallback(): Promise<SupabaseAuthSession | null> {
const expiresAt = resolveExpiry(hash.get("expires_at"), hash.get("expires_in"));
// Retira los tokens de la barra de direcciones antes de hacer más solicitudes.
// Retira los tokens y el parámetro temporal de la barra de direcciones.
clearOAuthNavigation();
cleanOAuthFragment();
const user = await fetchUser(accessToken);
@@ -184,6 +197,41 @@ async function fetchUser(accessToken: string): Promise<SupabaseAuthUser> {
return user as SupabaseAuthUser;
}
async function warmLatestApplicationEntry(url: string): Promise<void> {
try {
await fetch(url, {
method: "GET",
cache: "reload",
credentials: "same-origin",
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",
},
});
} catch {
// El login debe continuar aunque la actualización preventiva no responda.
}
}
function markOAuthNavigation(): void {
try {
sessionStorage.setItem(
OAUTH_NAVIGATION_KEY,
JSON.stringify({ startedAt: Date.now(), redirectUrl: getOAuthRedirectUrl() }),
);
} catch {
// Algunos navegadores pueden bloquear sessionStorage; el OAuth sigue funcionando.
}
}
function clearOAuthNavigation(): void {
try {
sessionStorage.removeItem(OAUTH_NAVIGATION_KEY);
} catch {
// No impide cerrar o restaurar la sesión.
}
}
function publicHeaders(): HeadersInit {
return {
apikey: supabaseKey,
@@ -233,10 +281,14 @@ function clearStoredSession(): void {
function cleanOAuthFragment(): void {
const cleanUrl = new URL(window.location.href);
cleanUrl.hash = "";
cleanUrl.searchParams.delete(OAUTH_FRESH_QUERY_KEY);
cleanUrl.searchParams.delete("oauth_return");
const search = cleanUrl.searchParams.toString();
window.history.replaceState(
null,
document.title,
`${cleanUrl.pathname}${cleanUrl.search}`,
`${cleanUrl.pathname}${search ? `?${search}` : ""}`,
);
}