Rediseño visual UABC + Mulish + auditoría de BD (P0/P1)
Rediseño visual: - Nueva identidad UABC/editorial (DESIGN.md), tipografía Mulish, tokens verde/dorado/negro sobre bone-white, patrón de puntos verde en canvas, radios 8/12/30/9999, sin sombras, hairline 1px, tarjetas con leve transparencia + blur suave + borde verde muy sutil. - 45 archivos .astro/.tsx alineados al nuevo sistema (Track A/B/C/D/E). - Limpieza post-critique: eliminado triple CTA en home alumno, .card anidados en admin, aria-live decorativo, 26 aliases legacy en global.css, border-l verde en nav (patrón AI-slop), classList.remove obsoletos. Auditoría de llamadas a BD (para 20-50 usuarios concurrentes): - P0-1: middleware con cache in-memory de profile (30s) + is_banned (30s) por uid; exclusión de assets estáticos (/_astro/*, /audio/*, favicon). Invalidación en signout, PATCH /profile, baneos. - P0-2: eliminado count pendientes duplicado en AppLayout; badge se hidrata client-side (BadgeSolicitudes con fetch inicial). - P0-3: migración 0008 REPLICA IDENTITY FULL en solicitudes + delta preciso en BadgeSolicitudes (con fallback a refetch). - P1: admin/index dedupe (4 queries → 2 con recientes.filter), catalogo reusa profile del middleware (3 queries → 1), inventario paralelo, estadisticas con cache 60s en RAM (stats-cache.ts), migración 0009 con 3 índices (fecha, vencidos parcial, audit compuesto). Fixes P0 de UI post-critique: - estadisticas.astro: ReferenceError DIAS restaurado. - solicitudes (3 vistas): error rendering sanitizado (no expone mensaje Postgres crudo al admin). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014xaxs8cGcQe9NwbMqA1gvn
This commit is contained in:
+64
-19
@@ -6,7 +6,33 @@ const PUBLIC_ROUTES = ['/login', '/api/auth/signin', '/api/auth/callback', '/api
|
||||
// Rutas siempre accesibles para un user baneado (sino, loop de rewrite)
|
||||
const BANNED_ALLOWED = ['/banned', '/api/auth/signout'];
|
||||
|
||||
// Caches en memoria del proceso — TTL corto, mono-container. Se invalidan en signout
|
||||
// y al actualizar profile. Reducen 2-3 hits a Postgres por request a ~0.05 en promedio.
|
||||
const PROFILE_TTL_MS = 30_000;
|
||||
const BANNED_TTL_MS = 30_000;
|
||||
type CachedProfile = { profile: any; exp: number };
|
||||
type CachedBanned = { banned: boolean; exp: number };
|
||||
const profileCache = new Map<string, CachedProfile>();
|
||||
const bannedCache = new Map<string, CachedBanned>();
|
||||
|
||||
export function invalidateUserCaches(uid: string) {
|
||||
profileCache.delete(uid);
|
||||
bannedCache.delete(uid);
|
||||
}
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
// Skip assets estáticos y audio — Astro los sirve por el mismo pipeline y no necesitan auth.
|
||||
const path = context.url.pathname;
|
||||
if (
|
||||
path.startsWith('/_astro/') ||
|
||||
path.startsWith('/audio/') ||
|
||||
path === '/favicon.svg' ||
|
||||
path === '/favicon.ico' ||
|
||||
path === '/robots.txt'
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const supabase = serverClient(context.cookies);
|
||||
context.locals.supabase = supabase;
|
||||
|
||||
@@ -21,27 +47,36 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
context.locals.profile = null;
|
||||
|
||||
if (user) {
|
||||
const select = 'id, email, nombre, matricula, rol, semestre, tutor_id, foto_path';
|
||||
let { data: profile } = await supabase
|
||||
.from('profiles')
|
||||
.select(select)
|
||||
.eq('id', user.id)
|
||||
.maybeSingle();
|
||||
const now = Date.now();
|
||||
const cached = profileCache.get(user.id);
|
||||
let profile: any = cached && cached.exp > now ? cached.profile : null;
|
||||
|
||||
// Self-heal: el trigger prestamos_on_auth_user_created coexiste con otro
|
||||
// trigger del proyecto vecino en auth.users y no siempre dispara. Si el
|
||||
// user existe pero no hay profile, lo creamos aquí con los datos del OAuth.
|
||||
if (!profile && user.email?.toLowerCase().endsWith(UABC_DOMAIN)) {
|
||||
const nombre =
|
||||
(user.user_metadata?.full_name as string | undefined) ??
|
||||
(user.user_metadata?.name as string | undefined) ??
|
||||
null;
|
||||
const { data: creado } = await supabase
|
||||
if (!profile) {
|
||||
const select = 'id, email, nombre, matricula, rol, semestre, tutor_id, foto_path';
|
||||
const { data } = await supabase
|
||||
.from('profiles')
|
||||
.upsert({ id: user.id, email: user.email, nombre }, { onConflict: 'id' })
|
||||
.select(select)
|
||||
.eq('id', user.id)
|
||||
.maybeSingle();
|
||||
profile = creado ?? null;
|
||||
profile = data ?? null;
|
||||
|
||||
// Self-heal: el trigger prestamos_on_auth_user_created coexiste con otro
|
||||
// trigger del proyecto vecino en auth.users y no siempre dispara. Si el
|
||||
// user existe pero no hay profile, lo creamos aquí con los datos del OAuth.
|
||||
if (!profile && user.email?.toLowerCase().endsWith(UABC_DOMAIN)) {
|
||||
const nombre =
|
||||
(user.user_metadata?.full_name as string | undefined) ??
|
||||
(user.user_metadata?.name as string | undefined) ??
|
||||
null;
|
||||
const { data: creado } = await supabase
|
||||
.from('profiles')
|
||||
.upsert({ id: user.id, email: user.email, nombre }, { onConflict: 'id' })
|
||||
.select(select)
|
||||
.maybeSingle();
|
||||
profile = creado ?? null;
|
||||
}
|
||||
|
||||
if (profile) profileCache.set(user.id, { profile, exp: now + PROFILE_TTL_MS });
|
||||
}
|
||||
|
||||
context.locals.profile = profile ?? null;
|
||||
@@ -60,9 +95,19 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
|
||||
// Ban guard: si el user está baneado (y no es admin), toda ruta cae a /banned
|
||||
// salvo la propia /banned y el signout. RPC is_banned respeta expires_at.
|
||||
// Cache 30s por uid — se invalida al banear/desbanear desde los endpoints admin.
|
||||
if (user && context.locals.profile && context.locals.profile.rol !== 'admin') {
|
||||
const { data: banned } = await supabase.rpc('is_banned', { p_uid: user.id });
|
||||
if (banned === true && !BANNED_ALLOWED.includes(pathname)) {
|
||||
const now = Date.now();
|
||||
const cached = bannedCache.get(user.id);
|
||||
let banned: boolean;
|
||||
if (cached && cached.exp > now) {
|
||||
banned = cached.banned;
|
||||
} else {
|
||||
const { data } = await supabase.rpc('is_banned', { p_uid: user.id });
|
||||
banned = data === true;
|
||||
bannedCache.set(user.id, { banned, exp: now + BANNED_TTL_MS });
|
||||
}
|
||||
if (banned && !BANNED_ALLOWED.includes(pathname)) {
|
||||
return context.rewrite('/banned');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user