Aqui você gerencia, inspira e faz o Projeto 1% chegar mais longe.
B
—
Alunas cadastradas
—
Novas alunas
—
Ativas nos últimos 30 dias
—
Pedidos de livros pendentes
Crescimento de cadastros
Novas alunas por período
Atividade no portal
Alunas que usaram o portal
Hoje—
Últimos 7 dias—
Últimos 30 dias—
Sem acessar há +30 dias—
Acesso rápido
Gerencie o portal em um toque
Lembrete do dia (atual)
—
Últimas alunas cadastradas
Carregando...
Solicitações de livros
Carregando...
Alunas
Aluna
Cadastro
Último acesso
Hábitos
Streak
Carregando alunas...
Pedidos de livros
Título
Autor
Aluna
Data
Status
Ação
Clique em "Pedidos de Livros" para carregar.
📖 Biblioteca de PDFs
Adicione um livro aqui e ele aparece automaticamente no Cantinho dos Livros de todas as alunas — sem precisar mexer em código.
📚 Adicionar Livro
Dica: no Google Drive, clique com botão direito no arquivo (a capa e o PDF podem estar em arquivos separados) → Compartilhar → "Qualquer pessoa com o link" → copie o link e cole aqui. Não precisa converter nada — o sistema ajusta o link da capa sozinho pra imagem aparecer certinho. Se preferir, pode usar links de outros sites de imagem também (ex: imgur.com, postimages.org).
✦ Salvo!
Livros na biblioteca
Capa
Título
Autor
Categoria
Ações
Clique em "Biblioteca" para carregar.
Menu e textos do portal
Desmarque o que você quer esconder do menu lateral das alunas. Nada é apagado: o conteúdo e o progresso delas continuam salvos, e é só marcar de novo para voltar. Boas-vindas e Avisos ficam sempre visíveis.
Quais páginas aparecem
Nomes e textos das páginas
Mude o nome no menu e o título/subtítulo de cada página. Deixe em branco para manter o original (que aparece de exemplo no campo). Use *asteriscos* para o itálico dourado, por exemplo: Mural de *Avisos*.
Sugestões das alunas
0
Recebidas
0
Não lidas
0
Resolvidas
Carregando...
Comunicação
Edite aqui e as alunas veem as mudanças em tempo real, sem precisar mexer no código.
💬 Lembrete do Dia
A frase inspiracional que aparece na home de todas as alunas.
✦ Salvo!
📣 Editor de Avisos
Formate o aviso do jeito que quiser — negrito, cores, listas, links, imagens. O aviso mais recente publicado (últimos 7 dias) aparece em destaque na home; todos ficam guardados no histórico.
Deixe vazio para publicar agora. Aviso agendado não envia push.
✦ Salvo!
👁️ Prévia — como as alunas vão ver
📣 Aviso
A prévia aparece aqui conforme você digita...
Histórico
Carregando...
Configurações
Notificação push
Avisa no celular das alunas quando você publica um aviso. —
Histórico de alterações
Tudo que foi feito pelo painel (lembrete, avisos, livros, menu, sugestões). Aparece depois que o SQL "0b" estiver rodado.
Carregando...
🛠️ SQL para rodar no Supabase
Rode esses SQLs uma vez no Supabase → SQL Editor para ativar todas as funcionalidades do painel.
0. BASE DO PAINEL — rode este primeiro
Sem este SQL o painel não consegue salvar lembrete e menu, nem ler sugestões e datas reais das alunas. Ele cadastra só a sua conta (barbaracelitti@gmail.com) como administradora. Pode rodar mais de uma vez.
-- ════════════════════════════════════════════════════════════════════
-- PROJETO 1% — BASE DO PAINEL SEM CHAVE SECRETA
-- O Supabase bloqueia chave secreta no navegador ("Forbidden use of secret API key in browser").
-- Então o painel passa a usar o SEU LOGIN: só a conta barbaracelitti@gmail.com é administradora.
-- Rode UMA vez (pode repetir sem problema). NÃO apaga dados nem mexe nas regras das alunas.
-- ════════════════════════════════════════════════════════════════════
begin;
-- 1) Lista de administradoras (só a sua conta)
create table if not exists public.admins (
user_id uuid primary key references auth.users(id) on delete cascade,
created_at timestamptz not null default now()
);
alter table public.admins enable row level security;
revoke all on public.admins from anon, authenticated;
insert into public.admins (user_id)
select id from auth.users where lower(email) = 'barbaracelitti@gmail.com'
on conflict do nothing;
do $$ begin
if not exists (select 1 from public.admins) then
raise exception 'Não achei a conta barbaracelitti@gmail.com em Authentication → Users. Nada foi alterado.';
end if;
end $$;
create or replace function public.is_admin()
returns boolean language sql stable security definer set search_path = public
as $$ select exists (select 1 from public.admins a where a.user_id = auth.uid()); $$;
revoke all on function public.is_admin() from public;
grant execute on function public.is_admin() to anon, authenticated;
-- 2) Lembrete e menu (portal_config): todas leem, só a administradora escreve
alter table public.portal_config enable row level security;
drop policy if exists "leitura_publica" on public.portal_config;
drop policy if exists "escrita_admin" on public.portal_config;
drop policy if exists "escrita_autenticada" on public.portal_config;
drop policy if exists "update_autenticado" on public.portal_config;
drop policy if exists "delete_autenticado" on public.portal_config;
drop policy if exists portal_config_ler on public.portal_config;
drop policy if exists portal_config_ins on public.portal_config;
drop policy if exists portal_config_upd on public.portal_config;
drop policy if exists portal_config_del on public.portal_config;
create policy portal_config_ler on public.portal_config for select to anon, authenticated using (true);
create policy portal_config_ins on public.portal_config for insert to authenticated with check (public.is_admin());
create policy portal_config_upd on public.portal_config for update to authenticated using (public.is_admin()) with check (public.is_admin());
create policy portal_config_del on public.portal_config for delete to authenticated using (public.is_admin());
grant select on public.portal_config to anon, authenticated;
grant insert, update, delete on public.portal_config to authenticated;
-- 3) Sugestões das alunas: a administradora lê tudo e muda o status
do $$ begin
if to_regclass('public.feedbacks_portal') is not null then
execute 'drop policy if exists feedbacks_admin_ler on public.feedbacks_portal';
execute 'drop policy if exists feedbacks_admin_upd on public.feedbacks_portal';
execute 'create policy feedbacks_admin_ler on public.feedbacks_portal for select to authenticated using (public.is_admin())';
execute 'create policy feedbacks_admin_upd on public.feedbacks_portal for update to authenticated using (public.is_admin()) with check (public.is_admin())';
execute 'grant update on public.feedbacks_portal to authenticated';
end if;
end $$;
-- 4) Datas reais de cadastro e último login (só a administradora consegue chamar)
drop function if exists public.admin_list_contas();
create function public.admin_list_contas()
returns table (user_id uuid, email text, cadastro_em timestamptz, ultimo_login timestamptz, ultima_atividade timestamptz)
language plpgsql stable security definer set search_path = public, auth
as $$
begin
if not public.is_admin() then
raise exception 'acesso negado' using errcode = '42501';
end if;
return query
select u.id, u.email::text, u.created_at, u.last_sign_in_at,
greatest(s.updated_at, p.updated_at, se.ultima_sessao)
from auth.users u
left join public.user_sync_data s on s.user_id = u.id
left join public.student_progress p on p.user_id = u.id
left join lateral (
select max(coalesce((to_jsonb(x)->>'refreshed_at')::timestamp, (to_jsonb(x)->>'updated_at')::timestamp, x.created_at::timestamp)) at time zone 'utc' as ultima_sessao
from auth.sessions x where x.user_id = u.id
) se on true
order by u.created_at desc;
end $$;
revoke all on function public.admin_list_contas() from public, anon;
grant execute on function public.admin_list_contas() to authenticated;
commit;
0b. SEGURANÇA DOS DADOS DAS ALUNAS — rode depois do 0
Troca as regras antigas e abertas (hoje qualquer pessoa consegue ler o progresso e os dados de todas as alunas, e qualquer aluna logada consegue editar avisos e livros) por regras justas: cada aluna só vê e altera os próprios dados, e só a administradora (você) gerencia avisos, livros, pedidos e vê todas. Tudo roda numa transação: se algo falhar, nada muda. Pode aparecer o aviso amarelo de "destructive operations": pode clicar em Run query.
-- ════════════════════════════════════════════════════════════════════
-- PROJETO 1% — ETAPA 2 · SEGURANÇA DO BANCO (Supabase)
-- Rode UMA vez: Supabase → SQL Editor → cole tudo → Run.
-- É seguro rodar de novo (não duplica nada).
--
-- O que isto faz:
-- 1. Cria a lista de administradoras (só a sua conta entra).
-- 2. Troca TODAS as regras antigas por regras justas:
-- · aluna só vê e altera os PRÓPRIOS dados;
-- · só administradora cria/edita/apaga avisos, livros, lembrete, desafios;
-- · rascunhos de avisos deixam de ser visíveis para as alunas;
-- · aluna não consegue mais mudar a própria data de vencimento do acesso.
-- 3. Cria funções seguras para o painel (lista de alunas com datas reais).
-- 4. Cria o histórico de ações do painel.
-- Tudo roda numa transação: se qualquer parte falhar, NADA muda.
-- ════════════════════════════════════════════════════════════════════
begin;
-- ── 1. ADMINISTRADORAS ───────────────────────────────────────────────
create table if not exists public.admins (
user_id uuid primary key references auth.users(id) on delete cascade,
created_at timestamptz not null default now()
);
alter table public.admins enable row level security;
-- Sem nenhuma policy de propósito: ninguém lê/edita esta tabela pelo app.
-- Só a função is_admin() (abaixo) consulta. Também tiramos a permissão de acesso por completo.
revoke all on public.admins from anon, authenticated;
insert into public.admins (user_id)
select id from auth.users where lower(email) = 'barbaracelitti@gmail.com'
on conflict do nothing;
do $$
begin
if not exists (select 1 from public.admins) then
raise exception 'Não achei a conta barbaracelitti@gmail.com em Authentication → Users. Nada foi alterado.';
end if;
end $$;
create or replace function public.is_admin()
returns boolean
language sql stable security definer
set search_path = public
as $$ select exists (select 1 from public.admins a where a.user_id = auth.uid()); $$;
revoke all on function public.is_admin() from public;
-- anon também precisa poder executar (as regras de leitura a consultam); para quem não é
-- admin a resposta é sempre "false" e nada é revelado.
grant execute on function public.is_admin() to anon, authenticated;
-- ── 2. APAGA AS REGRAS ANTIGAS (muitas eram "USING (true)") ──────────
do $$
declare r record;
begin
for r in
select schemaname, tablename, policyname
from pg_policies
where schemaname = 'public'
and tablename in ('portal_config','avisos_portal','livros_biblioteca','book_requests',
'student_progress','student_achievements','user_sync_data',
'lesson_progress','community_posts')
loop
execute format('drop policy %I on %I.%I', r.policyname, r.schemaname, r.tablename);
end loop;
end $$;
-- ── 3. REGRAS NOVAS ──────────────────────────────────────────────────
-- 3.1 portal_config (lembrete do dia, desafios) — leitura pública, escrita só admin
alter table public.portal_config enable row level security;
create policy portal_config_ler on public.portal_config for select to anon, authenticated using (true);
create policy portal_config_ins on public.portal_config for insert to authenticated with check (public.is_admin());
create policy portal_config_upd on public.portal_config for update to authenticated using (public.is_admin()) with check (public.is_admin());
create policy portal_config_del on public.portal_config for delete to authenticated using (public.is_admin());
-- 3.2 avisos_portal — alunas só veem avisos PUBLICADOS; rascunhos só a admin
alter table public.avisos_portal enable row level security;
create policy avisos_ler on public.avisos_portal for select to anon, authenticated
using (status = 'publicado' or public.is_admin());
create policy avisos_ins on public.avisos_portal for insert to authenticated with check (public.is_admin());
create policy avisos_upd on public.avisos_portal for update to authenticated using (public.is_admin()) with check (public.is_admin());
create policy avisos_del on public.avisos_portal for delete to authenticated using (public.is_admin());
-- 3.3 livros_biblioteca — leitura pública, escrita só admin
alter table public.livros_biblioteca enable row level security;
create policy livros_ler on public.livros_biblioteca for select to anon, authenticated using (true);
create policy livros_ins on public.livros_biblioteca for insert to authenticated with check (public.is_admin());
create policy livros_upd on public.livros_biblioteca for update to authenticated using (public.is_admin()) with check (public.is_admin());
create policy livros_del on public.livros_biblioteca for delete to authenticated using (public.is_admin());
-- 3.4 book_requests — aluna só PEDE (como "pendente"); ver/atender é só da admin
alter table public.book_requests enable row level security;
create policy pedidos_ins on public.book_requests for insert to authenticated
with check (coalesce(status, 'pendente') = 'pendente');
create policy pedidos_ler on public.book_requests for select to authenticated using (public.is_admin());
create policy pedidos_upd on public.book_requests for update to authenticated using (public.is_admin()) with check (public.is_admin());
create policy pedidos_del on public.book_requests for delete to authenticated using (public.is_admin());
-- 3.5 student_progress — cada aluna só o seu; a admin lê/edita todos
alter table public.student_progress enable row level security;
create policy progresso_ler on public.student_progress for select to authenticated
using (user_id = auth.uid() or public.is_admin());
create policy progresso_ins on public.student_progress for insert to authenticated
with check (user_id = auth.uid() or public.is_admin());
create policy progresso_upd on public.student_progress for update to authenticated
using (user_id = auth.uid() or public.is_admin())
with check (user_id = auth.uid() or public.is_admin());
create policy progresso_del on public.student_progress for delete to authenticated using (public.is_admin());
-- 3.6 student_achievements — cada aluna as suas; a admin lê todas
alter table public.student_achievements enable row level security;
create policy conquistas_ler on public.student_achievements for select to authenticated
using (user_id = auth.uid() or public.is_admin());
create policy conquistas_ins on public.student_achievements for insert to authenticated
with check (user_id = auth.uid() or public.is_admin());
create policy conquistas_upd on public.student_achievements for update to authenticated using (public.is_admin()) with check (public.is_admin());
create policy conquistas_del on public.student_achievements for delete to authenticated using (public.is_admin());
-- 3.7 user_sync_data (dados pessoais sincronizados) — SÓ a própria aluna.
-- Nem a admin lê direto: o painel usa funções que devolvem apenas totais.
alter table public.user_sync_data enable row level security;
create policy sync_ler on public.user_sync_data for select to authenticated using (user_id = auth.uid());
create policy sync_ins on public.user_sync_data for insert to authenticated with check (user_id = auth.uid());
create policy sync_upd on public.user_sync_data for update to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid());
create policy sync_del on public.user_sync_data for delete to authenticated using (user_id = auth.uid());
-- 3.8 Tabelas que podem ou não existir no seu projeto
do $$
begin
if to_regclass('public.lesson_progress') is not null then
execute 'alter table public.lesson_progress enable row level security';
execute 'create policy lessons_ler on public.lesson_progress for select to authenticated using (user_id = auth.uid() or public.is_admin())';
execute 'create policy lessons_ins on public.lesson_progress for insert to authenticated with check (user_id = auth.uid())';
execute 'create policy lessons_upd on public.lesson_progress for update to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid())';
execute 'create policy lessons_del on public.lesson_progress for delete to authenticated using (user_id = auth.uid() or public.is_admin())';
end if;
if to_regclass('public.community_posts') is not null then
-- o feed da comunidade não existe mais no app: fica trancado, só a admin enxerga
execute 'alter table public.community_posts enable row level security';
execute 'create policy comunidade_admin on public.community_posts for all to authenticated using (public.is_admin()) with check (public.is_admin())';
end if;
end $$;
-- ── 4. TRAVA: a aluna NÃO consegue estender o próprio acesso ─────────
create or replace function public.student_progress_guard()
returns trigger
language plpgsql security definer
set search_path = public
as $$
declare teto timestamptz := now() + interval '12 months 7 days';
begin
-- admin, webhook da Kiwify e Workers (chave de serviço) passam livres
if auth.uid() is null or public.is_admin() then return new; end if;
if tg_op = 'INSERT' then
if new.access_expires_at is not null then new.access_expires_at := least(new.access_expires_at, teto); end if;
if new.access_granted_at is not null then new.access_granted_at := least(new.access_granted_at, now()); end if;
else
if old.access_expires_at is not null then
new.access_expires_at := old.access_expires_at;
new.access_granted_at := old.access_granted_at;
else
if new.access_expires_at is not null then new.access_expires_at := least(new.access_expires_at, teto); end if;
if new.access_granted_at is not null then new.access_granted_at := least(new.access_granted_at, now()); end if;
end if;
end if;
return new;
end $$;
drop trigger if exists trg_student_progress_guard on public.student_progress;
create trigger trg_student_progress_guard
before insert or update on public.student_progress
for each row execute function public.student_progress_guard();
-- ── 5. LIMITES DE TAMANHO (só valem para gravações NOVAS) ────────────
do $$
begin
if not exists (select 1 from pg_constraint where conname = 'avisos_texto_tam') then
alter table public.avisos_portal add constraint avisos_texto_tam check (char_length(texto) <= 500000) not valid;
end if;
if not exists (select 1 from pg_constraint where conname = 'livros_campos_ok') then
alter table public.livros_biblioteca add constraint livros_campos_ok check (
char_length(titulo) <= 200 and char_length(coalesce(autor,'')) <= 200
and pdf_url ~* '^https?://' and (capa_url is null or capa_url = '' or capa_url ~* '^https?://')
) not valid;
end if;
if not exists (select 1 from pg_constraint where conname = 'pedidos_campos_ok') then
alter table public.book_requests add constraint pedidos_campos_ok check (
char_length(titulo) <= 200 and char_length(coalesce(autor,'')) <= 200
) not valid;
end if;
end $$;
-- ── 6. HISTÓRICO DE AÇÕES DO PAINEL ──────────────────────────────────
create table if not exists public.admin_audit_log (
id bigserial primary key,
admin_id uuid default auth.uid(),
acao text not null,
alvo text,
detalhes jsonb,
created_at timestamptz not null default now()
);
alter table public.admin_audit_log enable row level security;
drop policy if exists audit_ler on public.admin_audit_log;
drop policy if exists audit_ins on public.admin_audit_log;
create policy audit_ler on public.admin_audit_log for select to authenticated using (public.is_admin());
create policy audit_ins on public.admin_audit_log for insert to authenticated with check (public.is_admin());
-- sem update/delete: o histórico não pode ser alterado pelo app
revoke all on public.admin_audit_log from anon;
revoke update, delete, truncate on public.admin_audit_log from authenticated;
grant usage, select on sequence public.admin_audit_log_id_seq to authenticated;
-- ── 7. LISTA DE ALUNAS PARA O PAINEL (datas reais, só admin) ─────────
-- cadastro_em = data real em que a conta foi criada (Supabase Auth)
-- ultimo_login = último login com senha (não muda enquanto a sessão se renova sozinha)
-- ultima_atividade = última sincronização do progresso (melhor sinal de uso real)
create or replace function public.admin_list_students()
returns table (
user_id uuid, email text, cadastro_em timestamptz, ultimo_login timestamptz,
ultima_atividade timestamptz, progresso jsonb, conquistas text[]
)
language plpgsql stable security definer
set search_path = public, auth
as $$
begin
if not public.is_admin() then
raise exception 'acesso negado' using errcode = '42501';
end if;
return query
select u.id, u.email::text, u.created_at, u.last_sign_in_at, sp.updated_at,
coalesce(to_jsonb(sp) - 'xp' - 'level', '{}'::jsonb),
coalesce((select array_agg(a.achievement order by a.unlocked_at)
from public.student_achievements a where a.user_id = u.id), '{}'::text[])
from auth.users u
left join public.student_progress sp on sp.user_id = u.id
order by u.created_at desc;
end $$;
revoke all on function public.admin_list_students() from public, anon;
grant execute on function public.admin_list_students() to authenticated;
commit;
-- ── CONFERÊNCIA (rode depois, separado) ──────────────────────────────
-- select tablename, policyname, cmd, roles, qual, with_check
-- from pg_policies where schemaname = 'public' order by tablename, policyname;
-- select count(*) as administradoras from public.admins; -- deve dar 1
1. Tabela de Pedidos de Livros
CREATE TABLE IF NOT EXISTS book_requests (
id bigserial PRIMARY KEY,
titulo text NOT NULL,
autor text,
email_aluna text,
status text DEFAULT 'pendente',
created_at timestamptz DEFAULT now()
);
-- Permite que alunas autenticadas insiram pedidos
ALTER TABLE book_requests ENABLE ROW LEVEL SECURITY;
CREATE POLICY "alunas_insert" ON book_requests FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "admin_all" ON book_requests FOR ALL USING (true);
CREATE TABLE IF NOT EXISTS book_requests (
id bigserial PRIMARY KEY,
titulo text NOT NULL,
autor text,
email_aluna text,
status text DEFAULT 'pendente',
created_at timestamptz DEFAULT now()
);
ALTER TABLE book_requests ENABLE ROW LEVEL SECURITY;
CREATE POLICY "alunas_insert" ON book_requests FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "admin_all" ON book_requests FOR ALL USING (true);
0. ⚠️ Policies para o painel ver as alunas (rode PRIMEIRO se aparecer "Forbidden")
-- Permite que admin autenticada leia student_progress e achievements
CREATE POLICY IF NOT EXISTS "admin_read_progress" ON student_progress
FOR SELECT USING (true);
CREATE POLICY IF NOT EXISTS "admin_read_achievements" ON student_achievements
FOR SELECT USING (true);
-- Se as tabelas não tiverem RLS ativo:
ALTER TABLE student_progress ENABLE ROW LEVEL SECURITY;
ALTER TABLE student_achievements ENABLE ROW LEVEL SECURITY;
CREATE POLICY IF NOT EXISTS "admin_read_progress" ON student_progress FOR SELECT USING (true);
CREATE POLICY IF NOT EXISTS "admin_read_achievements" ON student_achievements FOR SELECT USING (true);
ALTER TABLE student_progress ENABLE ROW LEVEL SECURITY;
ALTER TABLE student_achievements ENABLE ROW LEVEL SECURITY;
2. Tabela de Controles do Portal (desafios, lembrete, aviso)
CREATE TABLE IF NOT EXISTS portal_config (
id text PRIMARY KEY,
valor jsonb,
updated_at timestamptz DEFAULT now()
);
ALTER TABLE portal_config ENABLE ROW LEVEL SECURITY;
-- Leitura pública (alunas e admin)
CREATE POLICY "leitura_publica" ON portal_config FOR SELECT USING (true);
-- Escrita para usuários autenticados (admin logada no painel)
CREATE POLICY "escrita_autenticada" ON portal_config FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "update_autenticado" ON portal_config FOR UPDATE TO authenticated USING (true);
CREATE POLICY "delete_autenticado" ON portal_config FOR DELETE TO authenticated USING (true);
CREATE TABLE IF NOT EXISTS portal_config (
id text PRIMARY KEY,
valor jsonb,
updated_at timestamptz DEFAULT now()
);
ALTER TABLE portal_config ENABLE ROW LEVEL SECURITY;
CREATE POLICY "leitura_publica" ON portal_config FOR SELECT USING (true);
CREATE POLICY "escrita_admin" ON portal_config FOR ALL USING (true);
3. Tabela da Biblioteca de Livros (PDFs)
CREATE TABLE IF NOT EXISTS livros_biblioteca (
id bigserial PRIMARY KEY,
categoria text NOT NULL,
categoria_label text,
titulo text NOT NULL,
autor text,
capa_url text,
pdf_url text NOT NULL,
created_at timestamptz DEFAULT now()
);
ALTER TABLE livros_biblioteca ENABLE ROW LEVEL SECURITY;
-- Leitura pública (todas as alunas veem os livros)
CREATE POLICY "leitura_publica_livros" ON livros_biblioteca FOR SELECT USING (true);
-- Escrita só para quem está logada no painel admin
CREATE POLICY "escrita_admin_livros" ON livros_biblioteca FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "update_admin_livros" ON livros_biblioteca FOR UPDATE TO authenticated USING (true);
CREATE POLICY "delete_admin_livros" ON livros_biblioteca FOR DELETE TO authenticated USING (true);
CREATE TABLE IF NOT EXISTS livros_biblioteca (
id bigserial PRIMARY KEY, categoria text NOT NULL, categoria_label text, titulo text NOT NULL,
autor text, capa_url text, pdf_url text NOT NULL, created_at timestamptz DEFAULT now()
);
ALTER TABLE livros_biblioteca ENABLE ROW LEVEL SECURITY;
CREATE POLICY "leitura_publica_livros" ON livros_biblioteca FOR SELECT USING (true);
CREATE POLICY "escrita_admin_livros" ON livros_biblioteca FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "update_admin_livros" ON livros_biblioteca FOR UPDATE TO authenticated USING (true);
CREATE POLICY "delete_admin_livros" ON livros_biblioteca FOR DELETE TO authenticated USING (true);
4. Tabela do Mural de Avisos
CREATE TABLE IF NOT EXISTS avisos_portal (
id bigserial PRIMARY KEY,
texto text NOT NULL,
arquivado boolean DEFAULT false,
status text DEFAULT 'publicado',
created_at timestamptz DEFAULT now()
);
-- Garante a coluna "status" mesmo se a tabela já existia antes do editor rico
ALTER TABLE avisos_portal ADD COLUMN IF NOT EXISTS status text DEFAULT 'publicado';
ALTER TABLE avisos_portal ENABLE ROW LEVEL SECURITY;
-- Leitura pública (todas as alunas veem o mural)
CREATE POLICY "leitura_publica_avisos" ON avisos_portal FOR SELECT USING (true);
-- Escrita só para quem está logada no painel admin
CREATE POLICY "escrita_admin_avisos" ON avisos_portal FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "update_admin_avisos" ON avisos_portal FOR UPDATE TO authenticated USING (true);
CREATE POLICY "delete_admin_avisos" ON avisos_portal FOR DELETE TO authenticated USING (true);
CREATE TABLE IF NOT EXISTS avisos_portal (
id bigserial PRIMARY KEY, texto text NOT NULL, arquivado boolean DEFAULT false, status text DEFAULT 'publicado', created_at timestamptz DEFAULT now()
);
ALTER TABLE avisos_portal ADD COLUMN IF NOT EXISTS status text DEFAULT 'publicado';
ALTER TABLE avisos_portal ENABLE ROW LEVEL SECURITY;
CREATE POLICY "leitura_publica_avisos" ON avisos_portal FOR SELECT USING (true);
CREATE POLICY "escrita_admin_avisos" ON avisos_portal FOR INSERT TO authenticated WITH CHECK (true);
CREATE POLICY "update_admin_avisos" ON avisos_portal FOR UPDATE TO authenticated USING (true);
CREATE POLICY "delete_admin_avisos" ON avisos_portal FOR DELETE TO authenticated USING (true);
5. Sugestões das alunas + avisos agendados (Etapa 4)
Rode este para ativar a caixa "Fale com a gente" do portal, a aba Sugestões e o agendamento de avisos. Só cria o que falta, não mexe em nada existente. Pode rodar mais de uma vez.
-- ═══════════════════════════════════════
-- PROJETO 1% — ETAPA 4 · Sugestões das alunas e avisos agendados
-- Rode UMA vez: Supabase → SQL Editor → cole tudo → Run. Pode rodar de novo sem duplicar.
-- Não mexe em nada que já existe (só cria o que falta).
-- O painel lê as sugestões com a chave de administradora; as alunas só podem
-- ENVIAR a própria sugestão (e ver as dela).
-- ═══════════════════════════════════════
-- 1) SUGESTÕES / FEEDBACKS
CREATE TABLE IF NOT EXISTS feedbacks_portal (
id bigserial PRIMARY KEY,
user_id uuid NOT NULL DEFAULT auth.uid() REFERENCES auth.users(id) ON DELETE CASCADE,
tipo text NOT NULL DEFAULT 'sugestao' CHECK (tipo IN ('sugestao','elogio','problema')),
mensagem text NOT NULL CHECK (char_length(mensagem) BETWEEN 3 AND 1000),
status text NOT NULL DEFAULT 'novo' CHECK (status IN ('novo','lido','resolvido')),
created_at timestamptz DEFAULT now()
);
ALTER TABLE feedbacks_portal ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "aluna_envia_feedback" ON feedbacks_portal;
DROP POLICY IF EXISTS "aluna_ve_proprio_feedback" ON feedbacks_portal;
CREATE POLICY "aluna_envia_feedback" ON feedbacks_portal FOR INSERT TO authenticated
WITH CHECK (user_id = auth.uid() AND status = 'novo');
CREATE POLICY "aluna_ve_proprio_feedback" ON feedbacks_portal FOR SELECT TO authenticated
USING (user_id = auth.uid());
REVOKE ALL ON feedbacks_portal FROM anon;
GRANT SELECT, INSERT ON feedbacks_portal TO authenticated;
GRANT USAGE ON SEQUENCE feedbacks_portal_id_seq TO authenticated;
-- 2) AVISOS AGENDADOS (a aluna só vê quando a data chega)
ALTER TABLE avisos_portal ADD COLUMN IF NOT EXISTS publicar_em timestamptz;
6. Proteger a tabela do portal (lembrete e menu)
Hoje a tabela portal_config está aberta: qualquer pessoa consegue alterar o lembrete e o menu. Rode este DEPOIS de subir este painel novo. As alunas continuam lendo normalmente; só o painel passa a poder escrever.
-- PROJETO 1% — Proteger a tabela portal_config (hoje está "UNRESTRICTED")
-- Hoje qualquer pessoa com a chave pública do portal consegue ALTERAR o lembrete do dia e o menu.
-- Depois disto: todas podem LER (o portal precisa), mas só o painel (chave de administradora) escreve.
-- Rode DEPOIS de subir o admin.html novo. Pode rodar mais de uma vez.
ALTER TABLE public.portal_config ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "leitura_publica" ON public.portal_config;
DROP POLICY IF EXISTS "escrita_admin" ON public.portal_config;
DROP POLICY IF EXISTS "escrita_autenticada" ON public.portal_config;
DROP POLICY IF EXISTS "update_autenticado" ON public.portal_config;
DROP POLICY IF EXISTS "delete_autenticado" ON public.portal_config;
CREATE POLICY "leitura_publica" ON public.portal_config FOR SELECT TO anon, authenticated USING (true);
REVOKE INSERT, UPDATE, DELETE ON public.portal_config FROM anon, authenticated;