From 7de4fe7054edfe9f6c9e10044815eae1e49e7bf1 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:34:12 +0000 Subject: [PATCH] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/integrations/supabase/types.ts | 164 +++++++++++++- ...5_18250a17-c2c8-45f8-b058-92506a1f0053.sql | 200 ++++++++++++++++++ 2 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 supabase/migrations/20260422143405_18250a17-c2c8-45f8-b058-92506a1f0053.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 89b7193..de10033 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -19,6 +19,9 @@ export type Database = { container_name: string | null created_at: string id: string + last_health_check_at: string | null + provisioned_at: string | null + railway_service_id: string | null status: string telegram_bot_token_vault_id: string | null telegram_bot_username: string | null @@ -32,11 +35,15 @@ export type Database = { user_id: string uuid_tenant: string vps_host: string | null + vps_pool_id: string | null } Insert: { container_name?: string | null created_at?: string id?: string + last_health_check_at?: string | null + provisioned_at?: string | null + railway_service_id?: string | null status?: string telegram_bot_token_vault_id?: string | null telegram_bot_username?: string | null @@ -50,11 +57,15 @@ export type Database = { user_id: string uuid_tenant?: string vps_host?: string | null + vps_pool_id?: string | null } Update: { container_name?: string | null created_at?: string id?: string + last_health_check_at?: string | null + provisioned_at?: string | null + railway_service_id?: string | null status?: string telegram_bot_token_vault_id?: string | null telegram_bot_username?: string | null @@ -68,6 +79,7 @@ export type Database = { user_id?: string uuid_tenant?: string vps_host?: string | null + vps_pool_id?: string | null } Relationships: [ { @@ -98,6 +110,13 @@ export type Database = { referencedRelation: "user_skill_limits" referencedColumns: ["user_id"] }, + { + foreignKeyName: "agent_instances_vps_pool_id_fkey" + columns: ["vps_pool_id"] + isOneToOne: false + referencedRelation: "vps_pool" + referencedColumns: ["id"] + }, ] } available_mcps: { @@ -351,6 +370,75 @@ export type Database = { } Relationships: [] } + provisioning_jobs: { + Row: { + agent_instance_id: string + attempt: number + completed_at: string | null + created_at: string + error_message: string | null + id: string + max_attempts: number + next_retry_at: string | null + payload: Json | null + railway_service_id: string | null + started_at: string | null + status: string + updated_at: string + user_id: string + vps_pool_id: string | null + } + Insert: { + agent_instance_id: string + attempt?: number + completed_at?: string | null + created_at?: string + error_message?: string | null + id?: string + max_attempts?: number + next_retry_at?: string | null + payload?: Json | null + railway_service_id?: string | null + started_at?: string | null + status?: string + updated_at?: string + user_id: string + vps_pool_id?: string | null + } + Update: { + agent_instance_id?: string + attempt?: number + completed_at?: string | null + created_at?: string + error_message?: string | null + id?: string + max_attempts?: number + next_retry_at?: string | null + payload?: Json | null + railway_service_id?: string | null + started_at?: string | null + status?: string + updated_at?: string + user_id?: string + vps_pool_id?: string | null + } + Relationships: [ + { + foreignKeyName: "provisioning_jobs_agent_instance_id_fkey" + columns: ["agent_instance_id"] + isOneToOne: false + referencedRelation: "agent_instances" + referencedColumns: ["id"] + }, + { + foreignKeyName: "provisioning_jobs_vps_pool_id_fkey" + columns: ["vps_pool_id"] + isOneToOne: false + referencedRelation: "vps_pool" + referencedColumns: ["id"] + }, + ] + } scheduled_jobs: { Row: { action_prompt: string @@ -924,6 +1012,69 @@ export type Database = { }, ] } + user_roles: { + Row: { + created_at: string + id: string + role: Database["public"]["Enums"]["app_role"] + user_id: string + } + Insert: { + created_at?: string + id?: string + role: Database["public"]["Enums"]["app_role"] + user_id: string + } + Update: { + created_at?: string + id?: string + role?: Database["public"]["Enums"]["app_role"] + user_id?: string + } + Relationships: [] + } + vps_pool: { + Row: { + capacity_current: number + capacity_max: number + created_at: string + id: string + is_active: boolean + name: string + notes: string | null + railway_environment_id: string | null + railway_project_id: string | null + region: string + updated_at: string + } + Insert: { + capacity_current?: number + capacity_max?: number + created_at?: string + id?: string + is_active?: boolean + name: string + notes?: string | null + railway_environment_id?: string | null + railway_project_id?: string | null + region?: string + updated_at?: string + } + Update: { + capacity_current?: number + capacity_max?: number + created_at?: string + id?: string + is_active?: boolean + name?: string + notes?: string | null + railway_environment_id?: string | null + railway_project_id?: string | null + region?: string + updated_at?: string + } + Relationships: [] + } } Views: { user_integration_limits: { @@ -959,6 +1110,13 @@ export type Database = { Args: { check_env?: string; user_uuid: string } Returns: boolean } + has_role: { + Args: { + _role: Database["public"]["Enums"]["app_role"] + _user_id: string + } + Returns: boolean + } vault_create_secret: { Args: { secret_description?: string @@ -978,7 +1136,7 @@ export type Database = { vault_delete_secret: { Args: { secret_id: string }; Returns: undefined } } Enums: { - [_ in never]: never + app_role: "admin" | "support" | "user" } CompositeTypes: { [_ in never]: never @@ -1105,6 +1263,8 @@ export type CompositeTypes< export const Constants = { public: { - Enums: {}, + Enums: { + app_role: ["admin", "support", "user"], + }, }, } as const diff --git a/supabase/migrations/20260422143405_18250a17-c2c8-45f8-b058-92506a1f0053.sql b/supabase/migrations/20260422143405_18250a17-c2c8-45f8-b058-92506a1f0053.sql new file mode 100644 index 0000000..a22e930 --- /dev/null +++ b/supabase/migrations/20260422143405_18250a17-c2c8-45f8-b058-92506a1f0053.sql @@ -0,0 +1,200 @@ +-- ========================================================================= +-- Fase 5.1 — Provisionamento Railway +-- ========================================================================= + +-- 1) Enum de roles + tabela user_roles + has_role() +CREATE TYPE public.app_role AS ENUM ('admin', 'support', 'user'); + +CREATE TABLE public.user_roles ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + role public.app_role NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (user_id, role) +); + +ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY; + +CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role public.app_role) +RETURNS boolean +LANGUAGE sql +STABLE +SECURITY DEFINER +SET search_path = public +AS $$ + SELECT EXISTS ( + SELECT 1 FROM public.user_roles + WHERE user_id = _user_id AND role = _role + ) +$$; + +CREATE POLICY "Usuários veem suas próprias roles" + ON public.user_roles FOR SELECT + USING (auth.uid() = user_id); + +CREATE POLICY "Admins veem todas as roles" + ON public.user_roles FOR SELECT + USING (public.has_role(auth.uid(), 'admin')); + +CREATE POLICY "Admins gerenciam roles" + ON public.user_roles FOR ALL + USING (public.has_role(auth.uid(), 'admin')) + WITH CHECK (public.has_role(auth.uid(), 'admin')); + +-- 2) vps_pool — pool de projetos Railway disponíveis para alocar serviços +CREATE TABLE public.vps_pool ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + name text NOT NULL, + railway_project_id text, + railway_environment_id text, + region text NOT NULL DEFAULT 'us-west', + capacity_max int NOT NULL DEFAULT 100, + capacity_current int NOT NULL DEFAULT 0, + is_active boolean NOT NULL DEFAULT true, + notes text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +ALTER TABLE public.vps_pool ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Admins gerenciam vps_pool" + ON public.vps_pool FOR ALL + USING (public.has_role(auth.uid(), 'admin')) + WITH CHECK (public.has_role(auth.uid(), 'admin')); + +CREATE TRIGGER update_vps_pool_updated_at + BEFORE UPDATE ON public.vps_pool + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- Seed inicial — admin precisa preencher os IDs do Railway +INSERT INTO public.vps_pool (name, railway_project_id, railway_environment_id, region, capacity_max, notes) +VALUES ( + 'railway-prod-1', + 'PREENCHER_APOS_CRIAR_NO_RAILWAY', + 'PREENCHER_APOS_CRIAR_NO_RAILWAY', + 'us-west', + 100, + 'Pool inicial. Admin: criar projeto no Railway e atualizar railway_project_id e railway_environment_id via SQL.' +); + +-- 3) Acréscimos em agent_instances para Railway +ALTER TABLE public.agent_instances + ADD COLUMN IF NOT EXISTS railway_service_id text, + ADD COLUMN IF NOT EXISTS vps_pool_id uuid REFERENCES public.vps_pool(id), + ADD COLUMN IF NOT EXISTS provisioned_at timestamptz, + ADD COLUMN IF NOT EXISTS last_health_check_at timestamptz; + +-- vps_host e container_name ficam no schema (compat) mas marcamos como deprecated via comment +COMMENT ON COLUMN public.agent_instances.vps_host IS 'DEPRECATED — substituído por vps_pool_id + railway_service_id'; +COMMENT ON COLUMN public.agent_instances.container_name IS 'DEPRECATED — substituído por railway_service_id'; + +-- 4) provisioning_jobs — log/state machine de cada tentativa de provisionamento +CREATE TABLE public.provisioning_jobs ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + agent_instance_id uuid NOT NULL REFERENCES public.agent_instances(id) ON DELETE CASCADE, + user_id uuid NOT NULL, + vps_pool_id uuid REFERENCES public.vps_pool(id), + railway_service_id text, + status text NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'running', 'retrying', 'completed', 'failed')), + attempt int NOT NULL DEFAULT 1, + max_attempts int NOT NULL DEFAULT 5, + error_message text, + payload jsonb, + started_at timestamptz, + completed_at timestamptz, + next_retry_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +ALTER TABLE public.provisioning_jobs ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Usuários veem seus próprios provisioning jobs" + ON public.provisioning_jobs FOR SELECT + USING (auth.uid() = user_id); + +CREATE POLICY "Admins veem todos os jobs" + ON public.provisioning_jobs FOR SELECT + USING (public.has_role(auth.uid(), 'admin')); + +CREATE INDEX idx_provisioning_jobs_agent_status ON public.provisioning_jobs(agent_instance_id, status); +CREATE INDEX idx_provisioning_jobs_retry ON public.provisioning_jobs(status, next_retry_at) WHERE status = 'retrying'; + +CREATE TRIGGER update_provisioning_jobs_updated_at + BEFORE UPDATE ON public.provisioning_jobs + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- 5) Extensão pg_net + trigger automático que dispara provision-agent +CREATE EXTENSION IF NOT EXISTS pg_net; + +CREATE OR REPLACE FUNCTION public.trigger_provision_agent() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, extensions +AS $$ +DECLARE + v_supabase_url text; + v_anon_key text; +BEGIN + -- Só dispara em INSERT com status='provisioning' ou em UPDATE que entra nesse status + IF NEW.status <> 'provisioning' THEN + RETURN NEW; + END IF; + + IF TG_OP = 'UPDATE' AND OLD.status = 'provisioning' THEN + RETURN NEW; + END IF; + + -- Lê config do projeto via vault (caímos no fallback se não existir) + BEGIN + SELECT decrypted_secret INTO v_supabase_url + FROM vault.decrypted_secrets WHERE name = 'project_url' LIMIT 1; + SELECT decrypted_secret INTO v_anon_key + FROM vault.decrypted_secrets WHERE name = 'anon_key' LIMIT 1; + EXCEPTION WHEN OTHERS THEN + v_supabase_url := NULL; + END; + + -- Sem config completa, não tenta — admin pode disparar manualmente via /admin + IF v_supabase_url IS NULL OR v_anon_key IS NULL THEN + RAISE LOG 'trigger_provision_agent: vault.project_url ou vault.anon_key não configurado, pulando'; + RETURN NEW; + END IF; + + PERFORM net.http_post( + url := v_supabase_url || '/functions/v1/provision-agent', + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'Authorization', 'Bearer ' || v_anon_key + ), + body := jsonb_build_object('agent_instance_id', NEW.id) + ); + + RETURN NEW; +END; +$$; + +CREATE TRIGGER on_agent_instance_provisioning + AFTER INSERT OR UPDATE OF status ON public.agent_instances + FOR EACH ROW + EXECUTE FUNCTION public.trigger_provision_agent(); + +-- Salvar URL e anon key do projeto no vault para o trigger usar +SELECT vault.create_secret('https://smsarmgoirlcedmqvdgc.supabase.co', 'project_url', 'URL do projeto Supabase usada pelo trigger pg_net'); + +-- 6) Ajustar policy de service_role poder gravar em agent_instances e provisioning_jobs +-- (as edge functions usam service_role; já bypassam RLS, mas precisamos garantir UPDATE) +CREATE POLICY "Service role gerencia agent_instances" + ON public.agent_instances FOR ALL + TO service_role + USING (true) + WITH CHECK (true); + +CREATE POLICY "Service role gerencia provisioning_jobs" + ON public.provisioning_jobs FOR ALL + TO service_role + USING (true) + WITH CHECK (true);