feat: actualizar proyecto completo

This commit is contained in:
2026-09-01 16:12:48 -04:00
parent ca6b111bd0
commit 6c848356ef
8 changed files with 304 additions and 58 deletions
+54
View File
@@ -0,0 +1,54 @@
# Permiso para crear proyectos — Tablero CDC
## Qué cambia
Se agrega el permiso `can_create_projects` en `public.tablero_cdc_allowed_users`.
Inicialmente queda activo únicamente para:
- Bonnie Oliva — `boliva@gomezleemarketing.com`
- Alicia Thaía Reyes — `areyes@gomezleemarketing.com`
- Isaac Daniel Aracena Toribio — `iaracena@gomezleemarketing.com`
- José Leopoldo Gomez — `jgomez@gomezleemarketing.com`
- Máximo Gomez — `mgomez@gomezleemarketing.com`
Para los demás usuarios activos, `can_create_projects=false`.
## Comportamiento
- Con permiso: aparece **+ Nuevo** y se puede crear un proyecto manualmente.
- Sin permiso: **+ Nuevo** no aparece.
- La protección no depende solo de la interfaz: una política RLS restrictiva exige el permiso para `INSERT` autenticados sobre `tablero_cdc_projects`.
- No se modifica la edición de proyectos existentes.
- No se modifica Tiempo, Horas del equipo, Tarifario, Listas, métricas, CDC Brief ni la sincronización con n8n/Google Sheets.
- El flujo de aprobación de CDC Brief sigue usando su RPC existente `SECURITY DEFINER`; este cambio está enfocado en la creación manual del Tablero.
## Instalación
1. Ejecutar `supabase_tablero_cdc_create_projects_permission.sql` en Supabase SQL Editor.
2. Confirmar que la consulta final muestre `can_create_projects=true` únicamente para los usuarios deseados.
3. Probar el `dist` actualizado.
4. Con una cuenta sin permiso, confirmar que **+ Nuevo** no aparece.
5. Con una cuenta con permiso, confirmar que **+ Nuevo** aparece y permite crear normalmente.
## Administración futura
Dar acceso:
```sql
update public.tablero_cdc_allowed_users
set can_create_projects = true,
updated_at = now()
where lower(email) = lower('correo@gomezleemarketing.com');
```
Quitar acceso:
```sql
update public.tablero_cdc_allowed_users
set can_create_projects = false,
updated_at = now()
where lower(email) = lower('correo@gomezleemarketing.com');
```
El cambio se refleja al volver a enfocar/recargar la aplicación.
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<link rel="icon" type="image/png" sizes="192x192" href="/tablero-cdc/icon-192.png?v=glm-tab-v12" /> <link rel="icon" type="image/png" sizes="192x192" href="/tablero-cdc/icon-192.png?v=glm-tab-v12" />
<link rel="shortcut icon" type="image/x-icon" href="/tablero-cdc/favicon.ico?v=glm-tab-v12" /> <link rel="shortcut icon" type="image/x-icon" href="/tablero-cdc/favicon.ico?v=glm-tab-v12" />
<link rel="apple-touch-icon" sizes="180x180" href="/tablero-cdc/apple-touch-icon.png?v=glm-tab-v12" /> <link rel="apple-touch-icon" sizes="180x180" href="/tablero-cdc/apple-touch-icon.png?v=glm-tab-v12" />
<script type="module" crossorigin src="/tablero-cdc/assets/index-CzoEmQMv.js"></script> <script type="module" crossorigin src="/tablero-cdc/assets/index-B-PY8au0.js"></script>
<link rel="stylesheet" crossorigin href="/tablero-cdc/assets/index-DbRgxteD.css"> <link rel="stylesheet" crossorigin href="/tablero-cdc/assets/index-DbRgxteD.css">
</head> </head>
<body> <body>
+6
View File
@@ -58,6 +58,7 @@ interface AuthContextValue {
canControlPricingSummary: boolean; canControlPricingSummary: boolean;
canManageTariffCatalog: boolean; canManageTariffCatalog: boolean;
canViewTeamHours: boolean; canViewTeamHours: boolean;
canCreateProjects: boolean;
loginWithGoogle: () => Promise<void>; loginWithGoogle: () => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
} }
@@ -204,6 +205,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
() => access?.canViewTeamHours === true, () => access?.canViewTeamHours === true,
[access?.canViewTeamHours], [access?.canViewTeamHours],
); );
const canCreateProjects = useMemo(
() => access?.canCreateProjects === true,
[access?.canCreateProjects],
);
return ( return (
<AuthContext.Provider <AuthContext.Provider
@@ -217,6 +222,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
canControlPricingSummary, canControlPricingSummary,
canManageTariffCatalog, canManageTariffCatalog,
canViewTeamHours, canViewTeamHours,
canCreateProjects,
loginWithGoogle, loginWithGoogle,
logout, logout,
}} }}
+28
View File
@@ -10,6 +10,7 @@ export type TableroCdcUserAccess = {
canControlPricingSummary: boolean; canControlPricingSummary: boolean;
canManageTariffCatalog: boolean; canManageTariffCatalog: boolean;
canViewTeamHours: boolean; canViewTeamHours: boolean;
canCreateProjects: boolean;
}; };
type AccessRow = { type AccessRow = {
@@ -22,6 +23,7 @@ type AccessRow = {
can_control_pricing_summary?: boolean | null; can_control_pricing_summary?: boolean | null;
can_manage_tariff_catalog?: boolean | null; can_manage_tariff_catalog?: boolean | null;
can_view_team_hours?: boolean | null; can_view_team_hours?: boolean | null;
can_create_projects?: boolean | null;
}; };
function normalizeEmail(email: string | null | undefined): string { function normalizeEmail(email: string | null | undefined): string {
@@ -43,6 +45,7 @@ function normalizeAccessRow(row: AccessRow | null | undefined): TableroCdcUserAc
canControlPricingSummary: row.can_control_pricing_summary === true, canControlPricingSummary: row.can_control_pricing_summary === true,
canManageTariffCatalog: row.can_manage_tariff_catalog === true, canManageTariffCatalog: row.can_manage_tariff_catalog === true,
canViewTeamHours: row.can_view_team_hours === true, canViewTeamHours: row.can_view_team_hours === true,
canCreateProjects: row.can_create_projects === true,
}; };
} }
@@ -53,6 +56,31 @@ export async function getActiveTableroCdcAccess(
if (!normalizedEmail) return null; if (!normalizedEmail) return null;
const queryWithCreatePermission = await supabase
.from("tablero_cdc_allowed_users")
.select(
"email, full_name, role, is_active, can_delete_projects, can_manage_internal_pricing, can_control_pricing_summary, can_manage_tariff_catalog, can_view_team_hours, can_create_projects",
)
.eq("email", normalizedEmail)
.eq("is_active", true)
.maybeSingle();
if (!queryWithCreatePermission.error) {
return normalizeAccessRow(queryWithCreatePermission.data as AccessRow | null);
}
const createPermissionMessage = queryWithCreatePermission.error.message || "";
const isMissingCreatePermission =
createPermissionMessage.includes("can_create_projects") ||
createPermissionMessage.includes("column") ||
createPermissionMessage.includes("schema cache");
if (!isMissingCreatePermission) {
throw queryWithCreatePermission.error;
}
// Compatibilidad durante el despliegue: si la columna nueva aún no existe,
// el resto de la app continúa funcionando, pero crear proyectos permanece bloqueado.
const queryWithTeamHoursPermission = await supabase const queryWithTeamHoursPermission = await supabase
.from("tablero_cdc_allowed_users") .from("tablero_cdc_allowed_users")
.select( .select(
+5
View File
@@ -1464,6 +1464,11 @@ async function addProject(project: Project & { pricingItems?: ProjectPricingItem
writeInProgress += 1; writeInProgress += 1;
try { try {
const access = await getCurrentTableroCdcAccess();
if (!access.canCreateProjects) {
throw new Error("No tienes permiso para crear proyectos.");
}
const userId = await getCurrentUserId(); const userId = await getCurrentUserId();
const insertPayload = { const insertPayload = {
...toProjectRow(project), ...toProjectRow(project),
+5 -1
View File
@@ -194,6 +194,7 @@ export default function BoardPage() {
canControlPricingSummary, canControlPricingSummary,
canManageTariffCatalog, canManageTariffCatalog,
canViewTeamHours, canViewTeamHours,
canCreateProjects,
logout, logout,
} = useAuth(); } = useAuth();
const briefUnread = useBriefUnreadCount(Boolean(user)); const briefUnread = useBriefUnreadCount(Boolean(user));
@@ -354,6 +355,7 @@ export default function BoardPage() {
}; };
const openNew = () => { const openNew = () => {
if (!canCreateProjects) return;
setEditing(null); setEditing(null);
setOpen(true); setOpen(true);
}; };
@@ -555,10 +557,12 @@ export default function BoardPage() {
</Button> </Button>
)} )}
{/* New project button */} {/* New project button — controlled by Supabase permission */}
{canCreateProjects && (
<Button onClick={openNew} className="gap-1.5 h-9 rounded-full px-4 shadow-sm"> <Button onClick={openNew} className="gap-1.5 h-9 rounded-full px-4 shadow-sm">
<Plus className="w-4 h-4" /> Nuevo <Plus className="w-4 h-4" /> Nuevo
</Button> </Button>
)}
</div> </div>
</header> </header>
@@ -0,0 +1,149 @@
-- =============================================================
-- TABLERO CDC — PERMISO PARA CREAR PROYECTOS / CUADROS
-- Fecha: 2026-09-01
-- =============================================================
-- Objetivo:
-- • Administrar desde Supabase quién puede crear proyectos manualmente.
-- • Mantener lectura, edición, Tiempo, Tarifario, Listas, Briefs y demás permisos intactos.
-- • Aplicar seguridad real en PostgreSQL, no solamente ocultar el botón "+ Nuevo".
--
-- Usuarios autorizados inicialmente:
-- • boliva@gomezleemarketing.com
-- • areyes@gomezleemarketing.com
-- • iaracena@gomezleemarketing.com
-- • jgomez@gomezleemarketing.com
-- • mgomez@gomezleemarketing.com
-- =============================================================
begin;
-- 1) Nuevo permiso, independiente de los demás.
alter table public.tablero_cdc_allowed_users
add column if not exists can_create_projects boolean not null default false;
comment on column public.tablero_cdc_allowed_users.can_create_projects is
'Permite crear manualmente nuevos proyectos/cuadros desde Tablero CDC.';
-- 2) Dejar exactamente a los cinco usuarios solicitados con acceso inicial.
-- No cambia role, is_active ni ningún otro permiso existente.
update public.tablero_cdc_allowed_users
set
can_create_projects = case
when lower(trim(email)) in (
'boliva@gomezleemarketing.com',
'areyes@gomezleemarketing.com',
'iaracena@gomezleemarketing.com',
'jgomez@gomezleemarketing.com',
'mgomez@gomezleemarketing.com'
) then true
else false
end,
updated_at = now();
-- 3) Función central de permiso, reutilizable por app/RLS.
create or replace function public.tablero_cdc_current_user_can_create_projects()
returns boolean
language sql
stable
security definer
set search_path = public
as $$
select exists (
select 1
from public.tablero_cdc_allowed_users u
where lower(trim(u.email)) = lower(trim(coalesce(auth.jwt() ->> 'email', '')))
and u.is_active = true
and u.can_create_projects = true
);
$$;
revoke all on function public.tablero_cdc_current_user_can_create_projects() from public;
grant execute on function public.tablero_cdc_current_user_can_create_projects() to authenticated;
-- 4) Candado de seguridad antes de tocar políticas de proyectos.
-- No habilitamos/deshabilitamos RLS ni reemplazamos políticas existentes.
-- Si la tabla no tiene la configuración esperada, toda esta transacción se revierte.
do $$
declare
v_rls_enabled boolean;
v_has_permissive_insert boolean;
begin
select c.relrowsecurity
into v_rls_enabled
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public'
and c.relname = 'tablero_cdc_projects'
and c.relkind in ('r', 'p');
if coalesce(v_rls_enabled, false) = false then
raise exception
'SEGURIDAD: tablero_cdc_projects no tiene RLS habilitado. No se aplicó ningún cambio.';
end if;
select exists (
select 1
from pg_policies p
where p.schemaname = 'public'
and p.tablename = 'tablero_cdc_projects'
and upper(p.permissive) = 'PERMISSIVE'
and upper(p.cmd) in ('INSERT', 'ALL')
) into v_has_permissive_insert;
if not v_has_permissive_insert then
raise exception
'SEGURIDAD: no se encontró la política permisiva de INSERT existente para tablero_cdc_projects. No se aplicó ningún cambio.';
end if;
end $$;
-- 5) Política RESTRICTIVA: se suma a las políticas existentes en vez de reemplazarlas.
-- Para los usuarios autenticados, un INSERT manual requiere can_create_projects=true.
-- Las operaciones realizadas con service_role continúan usando el comportamiento
-- normal de Supabase (service_role omite RLS).
drop policy if exists "tablero_cdc_projects_insert_requires_create_permission"
on public.tablero_cdc_projects;
create policy "tablero_cdc_projects_insert_requires_create_permission"
on public.tablero_cdc_projects
as restrictive
for insert
to authenticated
with check (public.tablero_cdc_current_user_can_create_projects());
commit;
-- =============================================================
-- VERIFICACIÓN (solo lectura)
-- =============================================================
select
email,
full_name,
role,
is_active,
can_create_projects
from public.tablero_cdc_allowed_users
order by full_name nulls last, email;
select
policyname,
permissive,
roles,
cmd,
with_check
from pg_policies
where schemaname = 'public'
and tablename = 'tablero_cdc_projects'
order by policyname;
-- =============================================================
-- ADMINISTRACIÓN FUTURA
-- =============================================================
-- Dar acceso:
-- update public.tablero_cdc_allowed_users
-- set can_create_projects = true, updated_at = now()
-- where lower(email) = lower('correo@gomezleemarketing.com');
--
-- Quitar acceso:
-- update public.tablero_cdc_allowed_users
-- set can_create_projects = false, updated_at = now()
-- where lower(email) = lower('correo@gomezleemarketing.com');