feat: actualizar proyecto completo
This commit is contained in:
@@ -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.
|
||||
+53
-53
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -11,7 +11,7 @@
|
||||
<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="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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -58,6 +58,7 @@ interface AuthContextValue {
|
||||
canControlPricingSummary: boolean;
|
||||
canManageTariffCatalog: boolean;
|
||||
canViewTeamHours: boolean;
|
||||
canCreateProjects: boolean;
|
||||
loginWithGoogle: () => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
@@ -204,6 +205,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
() => access?.canViewTeamHours === true,
|
||||
[access?.canViewTeamHours],
|
||||
);
|
||||
const canCreateProjects = useMemo(
|
||||
() => access?.canCreateProjects === true,
|
||||
[access?.canCreateProjects],
|
||||
);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
@@ -217,6 +222,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
canControlPricingSummary,
|
||||
canManageTariffCatalog,
|
||||
canViewTeamHours,
|
||||
canCreateProjects,
|
||||
loginWithGoogle,
|
||||
logout,
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type TableroCdcUserAccess = {
|
||||
canControlPricingSummary: boolean;
|
||||
canManageTariffCatalog: boolean;
|
||||
canViewTeamHours: boolean;
|
||||
canCreateProjects: boolean;
|
||||
};
|
||||
|
||||
type AccessRow = {
|
||||
@@ -22,6 +23,7 @@ type AccessRow = {
|
||||
can_control_pricing_summary?: boolean | null;
|
||||
can_manage_tariff_catalog?: boolean | null;
|
||||
can_view_team_hours?: boolean | null;
|
||||
can_create_projects?: boolean | null;
|
||||
};
|
||||
|
||||
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,
|
||||
canManageTariffCatalog: row.can_manage_tariff_catalog === 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;
|
||||
|
||||
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
|
||||
.from("tablero_cdc_allowed_users")
|
||||
.select(
|
||||
|
||||
@@ -1464,6 +1464,11 @@ async function addProject(project: Project & { pricingItems?: ProjectPricingItem
|
||||
writeInProgress += 1;
|
||||
|
||||
try {
|
||||
const access = await getCurrentTableroCdcAccess();
|
||||
if (!access.canCreateProjects) {
|
||||
throw new Error("No tienes permiso para crear proyectos.");
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId();
|
||||
const insertPayload = {
|
||||
...toProjectRow(project),
|
||||
|
||||
@@ -194,6 +194,7 @@ export default function BoardPage() {
|
||||
canControlPricingSummary,
|
||||
canManageTariffCatalog,
|
||||
canViewTeamHours,
|
||||
canCreateProjects,
|
||||
logout,
|
||||
} = useAuth();
|
||||
const briefUnread = useBriefUnreadCount(Boolean(user));
|
||||
@@ -354,6 +355,7 @@ export default function BoardPage() {
|
||||
};
|
||||
|
||||
const openNew = () => {
|
||||
if (!canCreateProjects) return;
|
||||
setEditing(null);
|
||||
setOpen(true);
|
||||
};
|
||||
@@ -555,10 +557,12 @@ export default function BoardPage() {
|
||||
</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">
|
||||
<Plus className="w-4 h-4" /> Nuevo
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</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');
|
||||
Reference in New Issue
Block a user