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="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>
|
||||||
|
|||||||
@@ -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,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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');
|
||||||
Reference in New Issue
Block a user