Publica versión definitiva de Seguimiento de Impuestos GLM
This commit is contained in:
+2
-28
@@ -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
|
||||
|
||||
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="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
@@ -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
@@ -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 .",
|
||||
|
||||
@@ -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">
|
||||
<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
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user