尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Amigo secreto (Secret Santa) para el parche, con dos presupuestos: la endulzada (dulces y mecato durante el mes) y el regalo grande del final. Next.js 16 (App Router) · TypeScript · Tailwind v4 · shadcn/ui · Supabase (Auth + Postgres + Storage) · listo para Vercel.檢視完整 README ↗
Amigo secreto (Secret Santa) para el parche, con dos presupuestos: la endulzada (dulces y mecato durante el mes) y el regalo grande del final.
Next.js 16 (App Router) · TypeScript · Tailwind v4 · shadcn/ui · Supabase (Auth + Postgres + Storage) · listo para Vercel.
groups.invite_code es un código de 8 caracteres
sin letras ambiguas (sin I, O, 0, 1). Quien abre /join/<code> ve el parche,
entra con su cuenta y confirma; public.join_group() le crea su propio
puesto. El admin no escribe nombres a mano./perfil, más una
wishlist privada (profile_wishlists) que se copia a cualquier parche con
public.import_profile_wishlist(). Se copia y no se enlaza: ajustar la lista
de un parche no le cambia el regalo a nadie más.public.perform_draw() acomoda los puestos en un
único ciclo hamiltoniano al azar, así que nadie se saca a sí mismo, cada uno
es sorteado exactamente una vez, y no quedan parejitas sueltas.members.assigned_to no tiene permiso de
lectura para ningún rol de cliente — ni para el admin. La única puerta es
public.get_my_assignment(), que solo devuelve tu propio resultado.Crea el proyecto en Supabase y corre, en este orden y en el SQL editor:
supabase/schema.sql
supabase/patches/001-revoke-execute-from-public.sql
supabase/patches/002-fix-rls-helper-execute.sql
supabase/patches/003-perfiles-y-enlace-de-parche.sql
Todos son idempotentes. Los patches no están plegados dentro de
schema.sql a propósito: mantener las dos cosas en paralelo es cómo se
terminan divergiendo.
Variables de entorno — copia .env.example a .env.local:
cp .env.example .env.local
NEXT_PUBLIC_SUPABASE_URL y NEXT_PUBLIC_SUPABASE_ANON_KEY salen de
Project Settings → API.
Correos de confirmación. En Authentication → Email Templates, apunta el enlace de confirmación a:
{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup
Para desarrollo también sirve apagar "Confirm email" en Authentication → Providers → Email: así el registro entra de una.
Arranca:
npm run dev
supabase/schema.sql tablas, RLS, grants por columna, RPCs, storage
src/proxy.ts refresca la sesión (era middleware.ts antes de Next 16)
src/lib/supabase/{server,client,env}.ts
src/lib/db.ts capa de lectura (todo pasa por acá)
src/lib/format.ts helpers puros — se pueden importar del cliente
src/lib/actions/*.ts Server Actions: auth, groups, members, wishlists
src/lib/site.ts el origen público, sacado de los headers
src/app/ /, /login, /dashboard, /g/[id], /join/[code], /perfil
RLS es la autoridad; los chequeos en TypeScript solo dan forma a la UI.
| Tabla | Lectura | Escritura |
|---|---|---|
profiles | solo el propio | solo el propio |
groups | admin o participante | solo admin (y solo name y presupuestos) |
members | cualquiera del grupo, sin assigned_to ni claim_token | cada quien se une por enlace; el admin quita antes del sorteo |
wishlists | la propia y la de quien te salió — nada más | solo la propia |
profile_wishlists | solo la propia — nunca sale del perfil | solo la propia |
Nombres, fotos y cumpleaños de los del parche salen por public.group_roster(),
porque profiles solo deja leer la fila propia. El celular no va en ese
roster: queda privado.
Dos detalles que valen la pena:
select a nivel de tabla en members y lo vuelve a otorgar columna por
columna, sin assigned_to ni claim_token. Un select * desde el cliente
lo rechaza Postgres, no la app.security definer a propósito: sin eso, las
políticas de members que consultan members se recursionan.EXECUTE. Una política se evalúa
con los privilegios de quien consulta, no del dueño de la tabla. Revocarle
EXECUTE a is_group_member() deja SELECT sobre groups, members y
wishlists fallando con 42501. Eso es lo que arregla el patch 002.EXECUTE a PUBLIC en cada función nueva; revoke ... from anon, authenticated no toca ese grant. Hay que nombrar a public.parseFloat sobre plata formateada. parseFloat("70.000") da 70
con separador de miles colombiano. Los montos viajan como dígitos puros.base-nova corre sobre Base UI, no Radix. No hay
asChild, es render={...}, y un Button que rinde un <a> necesita
nativeButton={false} (de ahí ButtonLink).Opcional: sin configurarlo la app funciona igual, solo que el interruptor del perfil no sirve y el cron responde diciendo qué variable falta.
Genera las llaves VAPID y guárdalas en Vercel:
npx web-push generate-vapid-keys
La pública va como NEXT_PUBLIC_VAPID_PUBLIC_KEY (el navegador la necesita
para suscribirse); la privada como VAPID_PRIVATE_KEY.
CRON_SECRET — cualquier cadena aleatoria (openssl rand -hex 24).
Protege /api/cron/reminders; en Vercel, definirla hace que Cron la mande
sola en el header Authorization.
SUPABASE_SERVICE_ROLE_KEY — Project Settings → API. Salta RLS por
completo y solo la usa el cron, que necesita ver los dispositivos de todos
para avisarles.
El horario vive en vercel.json: 0 13 * * * = 8:00 a.m. en Colombia. En
plan Hobby los crons corren una vez al día, que es justo lo que hace falta.
public.pending_reminders() calcula a quién y cuándo en una sola consulta;
cada envío queda anotado en reminder_log y la consulta descarta lo ya
anotado. Por eso el cron es idempotente: un reintento de Vercel no
duplica avisos./g/[id]/calendario, que baja un .ics
con las fechas y sus alarmas. El calendario del teléfono avisa sin permisos,
sin app y sin depender de que este servidor esté arriba.