Publica versión definitiva de Seguimiento de Impuestos GLM
This commit is contained in:
+2
-28
@@ -1,30 +1,4 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Build output
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Local environment
|
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.local
|
||||||
!.env.example
|
.env.production
|
||||||
*.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?
|
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
+9
File diff suppressed because one or more lines are too long
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
+9
File diff suppressed because one or more lines are too long
Vendored
+9
File diff suppressed because one or more lines are too long
Vendored
+44
-1
@@ -10,6 +10,49 @@
|
|||||||
/>
|
/>
|
||||||
<meta name="author" content="GomezLee Marketing" />
|
<meta name="author" content="GomezLee Marketing" />
|
||||||
<meta name="theme-color" content="#4F758B" />
|
<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:title" content="Seguimiento de Impuestos GLM" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
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="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="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" />
|
<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">
|
<link rel="stylesheet" crossorigin href="/calendario-impuestos/assets/index-DrJGHNJG.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+43
@@ -10,6 +10,49 @@
|
|||||||
/>
|
/>
|
||||||
<meta name="author" content="GomezLee Marketing" />
|
<meta name="author" content="GomezLee Marketing" />
|
||||||
<meta name="theme-color" content="#4F758B" />
|
<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:title" content="Seguimiento de Impuestos GLM" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc --noEmit && vite build",
|
"build": "tsc --noEmit && vite build && node scripts/create-legacy-asset-aliases.mjs",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
|||||||
@@ -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
@@ -1603,7 +1603,8 @@ function ContactManagerModal({
|
|||||||
<div className="rounded-xl border border-glm-blue/15 bg-glm-blue-soft/35 p-4">
|
<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="text-sm font-semibold text-glm-blue">Búsqueda automática</p>
|
||||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Field label="Nombre completo">
|
<Field label="Nombre completo">
|
||||||
@@ -1814,7 +1815,8 @@ function ContactForm({
|
|||||||
const invalid =
|
const invalid =
|
||||||
!draft.name.trim() ||
|
!draft.name.trim() ||
|
||||||
!draft.countryId ||
|
!draft.countryId ||
|
||||||
((draft.emailEnabled || draft.googleChatEnabled || draft.calendarEnabled) && !draft.email.trim());
|
((draft.emailEnabled || draft.googleChatEnabled || draft.calendarEnabled) &&
|
||||||
|
!draft.email.trim());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ export default function AuthGate() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogin = () => {
|
const handleLogin = async () => {
|
||||||
setActionPending(true);
|
setActionPending(true);
|
||||||
try {
|
try {
|
||||||
signInWithGoogle();
|
await signInWithGoogle();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setActionPending(false);
|
setActionPending(false);
|
||||||
setState({ status: "signed-out", error: getErrorMessage(error) });
|
setState({ status: "signed-out", error: getErrorMessage(error) });
|
||||||
@@ -192,7 +192,7 @@ export default function AuthGate() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleLogin}
|
onClick={() => void handleLogin()}
|
||||||
disabled={actionPending}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const anonKey = String(import.meta.env.VITE_SUPABASE_ANON_KEY ?? "").trim();
|
|||||||
const supabaseKey = publishableKey || anonKey;
|
const supabaseKey = publishableKey || anonKey;
|
||||||
|
|
||||||
const SESSION_STORAGE_KEY = "glm-tax-auth-session-v1";
|
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;
|
const EXPIRY_MARGIN_SECONDS = 60;
|
||||||
|
|
||||||
export const isSupabaseConfigured = Boolean(supabaseUrl && supabaseKey);
|
export const isSupabaseConfigured = Boolean(supabaseUrl && supabaseKey);
|
||||||
@@ -48,22 +50,30 @@ export function getSupabaseConfigurationMessage(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getOAuthRedirectUrl(): string {
|
export function getOAuthRedirectUrl(): string {
|
||||||
// La URL debe coincidir exactamente con una de las permitidas en Supabase.
|
// Se conserva exactamente la misma URL que ya está permitida en Supabase.
|
||||||
// No se agregan parámetros variables porque Auth descartaría redirect_to y
|
// La actualización del documento se hace después del retorno, desde el
|
||||||
// terminaría enviando al SITE_URL de Supabase.
|
// navegador, por lo que no requiere agregar una URL nueva en EasyPanel.
|
||||||
return new URL(import.meta.env.BASE_URL, window.location.origin).toString();
|
return new URL(import.meta.env.BASE_URL, window.location.origin).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function signInWithGoogle(): void {
|
export async function signInWithGoogle(): Promise<void> {
|
||||||
assertConfigured();
|
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({
|
const params = new URLSearchParams({
|
||||||
provider: "google",
|
provider: "google",
|
||||||
redirect_to: getOAuthRedirectUrl(),
|
redirect_to: redirectUrl,
|
||||||
prompt: "select_account",
|
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> {
|
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.
|
// El cierre local debe completarse incluso si Supabase no está disponible.
|
||||||
} finally {
|
} finally {
|
||||||
clearStoredSession();
|
clearStoredSession();
|
||||||
|
clearOAuthNavigation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +132,7 @@ async function consumeOAuthCallback(): Promise<SupabaseAuthSession | null> {
|
|||||||
const oauthError = hash.get("error_description") ?? hash.get("error");
|
const oauthError = hash.get("error_description") ?? hash.get("error");
|
||||||
|
|
||||||
if (oauthError) {
|
if (oauthError) {
|
||||||
|
clearOAuthNavigation();
|
||||||
cleanOAuthFragment();
|
cleanOAuthFragment();
|
||||||
throw new Error(oauthError.replace(/\+/g, " "));
|
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"));
|
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();
|
cleanOAuthFragment();
|
||||||
|
|
||||||
const user = await fetchUser(accessToken);
|
const user = await fetchUser(accessToken);
|
||||||
@@ -184,6 +197,41 @@ async function fetchUser(accessToken: string): Promise<SupabaseAuthUser> {
|
|||||||
return user as 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 {
|
function publicHeaders(): HeadersInit {
|
||||||
return {
|
return {
|
||||||
apikey: supabaseKey,
|
apikey: supabaseKey,
|
||||||
@@ -233,10 +281,14 @@ function clearStoredSession(): void {
|
|||||||
function cleanOAuthFragment(): void {
|
function cleanOAuthFragment(): void {
|
||||||
const cleanUrl = new URL(window.location.href);
|
const cleanUrl = new URL(window.location.href);
|
||||||
cleanUrl.hash = "";
|
cleanUrl.hash = "";
|
||||||
|
cleanUrl.searchParams.delete(OAUTH_FRESH_QUERY_KEY);
|
||||||
|
cleanUrl.searchParams.delete("oauth_return");
|
||||||
|
|
||||||
|
const search = cleanUrl.searchParams.toString();
|
||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
null,
|
null,
|
||||||
document.title,
|
document.title,
|
||||||
`${cleanUrl.pathname}${cleanUrl.search}`,
|
`${cleanUrl.pathname}${search ? `?${search}` : ""}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user