Harden auth, payments, and frontend against review findings.

Close exploitable gaps in booking/payment flows, enforce token versioning and account checks, gate sensitive payment data, and add middleware plus input validation across admin routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-06-24 19:59:02 +00:00
co-authored by Cursor
parent fc4af38e8a
commit a6840ea953
37 changed files with 1432 additions and 528 deletions
@@ -3,6 +3,7 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useAuth } from '@/context/AuthContext';
import { useLanguage } from '@/context/LanguageContext';
import { safeInternalPath } from '@/lib/safeRedirect';
import toast from 'react-hot-toast';
declare global {
@@ -82,10 +83,9 @@ export default function GoogleSignInButton({
toast.success(locale === 'es' ? 'Bienvenido!' : 'Welcome!');
onSuccess?.();
// Use window.location for navigation to ensure clean state
if (redirectTo) {
window.location.href = redirectTo;
}
// Use window.location for navigation to ensure clean state.
// Constrain to a same-origin path to avoid open redirects.
window.location.href = safeInternalPath(redirectTo, '/dashboard');
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Google login failed';
const displayError = locale === 'es' ? 'Error al iniciar sesion con Google' : errorMessage;
@@ -21,6 +21,17 @@ function extractLastUpdated(contentMarkdown: string, updatedAt?: string): string
return match ? match[1].trim() : updatedAt;
}
// Only permit safe link schemes. Anything else (javascript:, data:, etc.) is dropped
// so a malicious markdown link can't execute script when clicked.
function sanitizeHref(href?: string): string | undefined {
if (!href) return undefined;
const trimmed = href.trim();
// Allow relative/anchor/protocol-relative-safe links
if (trimmed.startsWith('/') || trimmed.startsWith('#')) return trimmed;
if (/^(https?:|mailto:|tel:)/i.test(trimmed)) return trimmed;
return undefined;
}
export default function LegalPageLayout({
slug,
initialLocale,
@@ -141,17 +152,20 @@ export default function LegalPageLayout({
{children}
</li>
),
// Style links
a: ({ href, children }) => (
<a
href={href}
className="text-primary-dark underline hover:text-primary-yellow transition-colors"
target={href?.startsWith('http') ? '_blank' : undefined}
rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
>
{children}
</a>
),
// Style links (with scheme allowlist to block javascript:/data: URLs)
a: ({ href, children }) => {
const safeHref = sanitizeHref(href);
return (
<a
href={safeHref}
className="text-primary-dark underline hover:text-primary-yellow transition-colors"
target={safeHref?.startsWith('http') ? '_blank' : undefined}
rel={safeHref?.startsWith('http') ? 'noopener noreferrer' : undefined}
>
{children}
</a>
);
},
// Style horizontal rules
hr: () => (
<hr className="my-8 border-gray-200" />
@@ -14,11 +14,18 @@ interface RichTextEditorProps {
editable?: boolean;
}
// Escape HTML-significant characters so any raw HTML embedded in the markdown source
// is neutralised before we layer our own generated tags on top (defense-in-depth;
// the public renderer escapes too, and TipTap sanitizes via its schema).
function escapeRawHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Convert markdown to HTML for TipTap
function markdownToHtml(markdown: string): string {
if (!markdown) return '<p></p>';
let html = markdown;
let html = escapeRawHtml(markdown);
// Convert horizontal rules first (before other processing)
html = html.replace(/^---+$/gm, '<hr>');