This commit is contained in:
1
supabase/.temp/cli-latest
Normal file
1
supabase/.temp/cli-latest
Normal file
@@ -0,0 +1 @@
|
||||
v2.109.0
|
||||
1
supabase/.temp/linked-project.json
Normal file
1
supabase/.temp/linked-project.json
Normal file
@@ -0,0 +1 @@
|
||||
{"ref":"xgqbkxcliyjzisoccsur","name":"inq-roi-desa","organization_id":"ekmgshaupdqaebwypwwj","organization_slug":"ekmgshaupdqaebwypwwj"}
|
||||
424
supabase/docker-compose.yml
Normal file
424
supabase/docker-compose.yml
Normal file
@@ -0,0 +1,424 @@
|
||||
# Usage
|
||||
# Start: docker compose up -d
|
||||
# Stop: docker compose down
|
||||
# Reset everything: sh reset.sh
|
||||
#
|
||||
# Variables de entorno específicas de InQ ROI (agregar a .env además de las de InQ Sizing):
|
||||
# APP_CODE_PATH — path absoluto al directorio code/ del app en Dokploy.
|
||||
# Se obtiene después de crear la aplicación InQ ROI en Dokploy.
|
||||
# Ejemplo: /etc/dokploy/applications/inq-roi-app-abc123/code
|
||||
# GOTRUE_URI_ALLOW_LIST — URLs permitidas para redirect OAuth.
|
||||
# Ejemplo: https://inq-roi.inqualityhq.com/**,http://localhost:5173/**
|
||||
|
||||
name: supabase
|
||||
|
||||
services:
|
||||
|
||||
studio:
|
||||
image: supabase/studio:2026.06.03-sha-0bca601
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"node -e \"fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})\""
|
||||
]
|
||||
timeout: 10s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
environment:
|
||||
HOSTNAME: "0.0.0.0"
|
||||
|
||||
STUDIO_PG_META_URL: http://meta:8080
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
|
||||
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||
PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000}
|
||||
PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public}
|
||||
|
||||
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
|
||||
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY}
|
||||
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY}
|
||||
|
||||
ENABLED_FEATURES_LOGS_ALL: "false"
|
||||
|
||||
SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
|
||||
EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions
|
||||
volumes:
|
||||
- ./volumes/snippets:/app/snippets:z
|
||||
# [InQ ROI] path al repo del app en Dokploy — configurar APP_CODE_PATH en .env
|
||||
- ${APP_CODE_PATH}/supabase/functions:/app/edge-functions:ro,z
|
||||
|
||||
kong:
|
||||
image: kong/kong:3.9.1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- api-gw
|
||||
dokploy-network: {}
|
||||
healthcheck:
|
||||
test: ["CMD", "kong", "health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
depends_on:
|
||||
studio:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- ${KONG_HTTP_PORT}:8000/tcp
|
||||
- ${KONG_HTTPS_PORT}:8443/tcp
|
||||
volumes:
|
||||
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
|
||||
- ./volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z
|
||||
environment:
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
KONG_DNS_NOT_FOUND_TTL: 1
|
||||
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function
|
||||
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
||||
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
||||
KONG_PROXY_ACCESS_LOG: /dev/stdout combined
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-}
|
||||
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-}
|
||||
ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-}
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-}
|
||||
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
|
||||
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||
entrypoint: /home/kong/kong-entrypoint.sh
|
||||
|
||||
auth:
|
||||
image: supabase/gotrue:v2.189.0
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:9999/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
GOTRUE_API_HOST: 0.0.0.0
|
||||
GOTRUE_API_PORT: 9999
|
||||
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
|
||||
|
||||
GOTRUE_DB_DRIVER: postgres
|
||||
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
|
||||
GOTRUE_SITE_URL: ${SITE_URL}
|
||||
# [InQ ROI] configurar en .env — ej: https://inq-roi.inqualityhq.com/**,http://localhost:5173/**
|
||||
GOTRUE_URI_ALLOW_LIST: ${GOTRUE_URI_ALLOW_LIST}
|
||||
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
|
||||
|
||||
GOTRUE_JWT_ADMIN_ROLES: service_role
|
||||
GOTRUE_JWT_AUD: authenticated
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
|
||||
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
||||
GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL}/auth/v1
|
||||
|
||||
GOTRUE_EXTERNAL_EMAIL_ENABLED: true
|
||||
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
|
||||
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
|
||||
|
||||
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
|
||||
GOTRUE_SMTP_HOST: ${SMTP_HOST}
|
||||
GOTRUE_SMTP_PORT: ${SMTP_PORT}
|
||||
GOTRUE_SMTP_USER: ${SMTP_USER}
|
||||
GOTRUE_SMTP_PASS: ${SMTP_PASS}
|
||||
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
|
||||
GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
|
||||
GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
|
||||
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}
|
||||
|
||||
GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
|
||||
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
|
||||
|
||||
# Google OAuth — credenciales en .env (misma Google Cloud Console app o una nueva)
|
||||
GOTRUE_EXTERNAL_GOOGLE_ENABLED: "true"
|
||||
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: "${GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID}"
|
||||
GOTRUE_EXTERNAL_GOOGLE_SECRET: "${GOTRUE_EXTERNAL_GOOGLE_SECRET}"
|
||||
# [InQ ROI] redirect URI usa API_EXTERNAL_URL — solo cambiar ese valor en .env
|
||||
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: "${API_EXTERNAL_URL}/auth/v1/callback"
|
||||
|
||||
rest:
|
||||
image: postgrest/postgrest:v14.12
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||
PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000}
|
||||
PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public}
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
PGRST_JWT_SECRET: ${JWT_JWKS:-${JWT_SECRET}}
|
||||
PGRST_DB_USE_LEGACY_GUCS: "false"
|
||||
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
|
||||
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
|
||||
command: ["postgrest"]
|
||||
|
||||
realtime:
|
||||
image: supabase/realtime:v2.102.3
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -sSfL --head -o /dev/null -H \"Authorization: Bearer ${ANON_KEY}\" http://localhost:4000/api/tenants/realtime-dev/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 30s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
environment:
|
||||
PORT: 4000
|
||||
DB_HOST: ${POSTGRES_HOST}
|
||||
DB_PORT: ${POSTGRES_PORT}
|
||||
DB_USER: supabase_admin
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
||||
DB_ENC_KEY: supabaserealtime
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
|
||||
METRICS_JWT_SECRET: ${JWT_SECRET}
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
DNS_NODES: "''"
|
||||
RLIMIT_NOFILE: "10000"
|
||||
APP_NAME: realtime
|
||||
SEED_SELF_HOST: "true"
|
||||
RUN_JANITOR: "true"
|
||||
DISABLE_HEALTHCHECK_LOGGING: "true"
|
||||
|
||||
storage:
|
||||
image: supabase/storage-api:v1.60.4
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
imgproxy:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://storage:5000/status"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
environment:
|
||||
ANON_KEY: ${ANON_KEY}
|
||||
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
POSTGREST_URL: http://rest:3000
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
STORAGE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
REQUEST_ALLOW_X_FORWARDED_PATH: "true"
|
||||
FILE_SIZE_LIMIT: 52428800
|
||||
STORAGE_BACKEND: file
|
||||
GLOBAL_S3_BUCKET: ${GLOBAL_S3_BUCKET}
|
||||
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
||||
TENANT_ID: ${STORAGE_TENANT_ID}
|
||||
REGION: ${REGION}
|
||||
ENABLE_IMAGE_TRANSFORMATION: "true"
|
||||
IMGPROXY_URL: http://imgproxy:5001
|
||||
S3_PROTOCOL_ACCESS_KEY_ID: ${S3_PROTOCOL_ACCESS_KEY_ID}
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET: ${S3_PROTOCOL_ACCESS_KEY_SECRET}
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:z
|
||||
|
||||
imgproxy:
|
||||
image: darthsim/imgproxy:v3.30.1
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:z
|
||||
healthcheck:
|
||||
test: ["CMD", "imgproxy", "health"]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
environment:
|
||||
IMGPROXY_BIND: ":5001"
|
||||
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
|
||||
IMGPROXY_USE_ETAG: "true"
|
||||
IMGPROXY_AUTO_WEBP: ${IMGPROXY_AUTO_WEBP}
|
||||
IMGPROXY_MAX_SRC_RESOLUTION: 16.8
|
||||
|
||||
meta:
|
||||
image: supabase/postgres-meta:v0.96.6
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PG_META_PORT: 8080
|
||||
PG_META_DB_HOST: ${POSTGRES_HOST}
|
||||
PG_META_DB_PORT: ${POSTGRES_PORT}
|
||||
PG_META_DB_NAME: ${POSTGRES_DB}
|
||||
PG_META_DB_USER: supabase_admin
|
||||
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
|
||||
|
||||
functions:
|
||||
image: supabase/edge-runtime:v1.74.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# [InQ ROI] path al repo del app en Dokploy — configurar APP_CODE_PATH en .env
|
||||
- ${APP_CODE_PATH}/supabase/functions:/home/deno/functions:z
|
||||
- deno-cache:/root/.cache/deno
|
||||
depends_on:
|
||||
kong:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "timeout 1 bash -c '</dev/tcp/127.0.0.1/9000'"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
environment:
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
SUPABASE_PUBLISHABLE_KEYS: "{\"default\":\"${SUPABASE_PUBLISHABLE_KEY:-}\"}"
|
||||
SUPABASE_SECRET_KEYS: "{\"default\":\"${SUPABASE_SECRET_KEY:-}\"}"
|
||||
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
# [InQ ROI] SITE_URL se inyecta en las Edge Functions como variable de entorno
|
||||
SITE_URL: ${SITE_URL}
|
||||
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
|
||||
command:
|
||||
[
|
||||
"start",
|
||||
"--main-service",
|
||||
"/home/deno/functions/main"
|
||||
]
|
||||
|
||||
db:
|
||||
image: supabase/postgres:15.8.1.085
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
|
||||
- ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
|
||||
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
|
||||
- ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
|
||||
- db-data:/var/lib/postgresql/data:Z
|
||||
- ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z
|
||||
- ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
|
||||
- ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z
|
||||
- db-config:/etc/postgresql-custom
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "postgres", "-h", "localhost"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_HOST: /var/run/postgresql
|
||||
PGPORT: ${POSTGRES_PORT}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
PGDATABASE: ${POSTGRES_DB}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_EXP: ${JWT_EXPIRY}
|
||||
command:
|
||||
[
|
||||
"postgres",
|
||||
"-c", "config_file=/etc/postgresql/postgresql.conf",
|
||||
"-c", "log_min_messages=fatal"
|
||||
]
|
||||
|
||||
supavisor:
|
||||
image: supabase/supavisor:2.9.5
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${POSTGRES_PORT}:5432
|
||||
- ${POOLER_PROXY_PORT_TRANSACTION}:6543
|
||||
volumes:
|
||||
- ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null",
|
||||
"http://127.0.0.1:4000/api/health"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PORT: 4000
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
|
||||
CLUSTER_POSTGRES: "true"
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
|
||||
VAULT_ENC_KEY: ${VAULT_ENC_KEY}
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
METRICS_JWT_SECRET: ${JWT_SECRET}
|
||||
REGION: local
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
POOLER_TENANT_ID: ${POOLER_TENANT_ID}
|
||||
POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE}
|
||||
POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN}
|
||||
POOLER_POOL_MODE: transaction
|
||||
DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE}
|
||||
command:
|
||||
[
|
||||
"/bin/sh", "-c",
|
||||
"/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"
|
||||
]
|
||||
|
||||
volumes:
|
||||
db-config:
|
||||
deno-cache:
|
||||
db-data:
|
||||
|
||||
networks:
|
||||
default: {}
|
||||
dokploy-network:
|
||||
external: true
|
||||
195
supabase/functions/invite-user/index.ts
Normal file
195
supabase/functions/invite-user/index.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Edge Function: invite-user
|
||||
* Propósito: invitar usuario externo (client_editor/client_viewer) o interno (member)
|
||||
* Solo accesible por platform_admin.
|
||||
* Variables de entorno requeridas: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SITE_URL
|
||||
* - SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY: disponibles automáticamente en Edge Functions
|
||||
* - SITE_URL: agregar manualmente en Supabase Dashboard → Edge Functions → Secrets
|
||||
* Valor producción: https://inq-roi.inqualityhq.com
|
||||
* Deploy: supabase functions deploy invite-user
|
||||
*/
|
||||
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
interface InviteUserRequest {
|
||||
email: string
|
||||
name: string
|
||||
platform_role: 'client_editor' | 'client_viewer' | 'member'
|
||||
org_name?: string
|
||||
}
|
||||
|
||||
const ALLOWED_ROLES = ['client_editor', 'client_viewer', 'member'] as const
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': Deno.env.get('SITE_URL') ?? '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
// Manejar CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers })
|
||||
}
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
return new Response(JSON.stringify({ error: 'Método no permitido' }), { status: 405, headers })
|
||||
}
|
||||
|
||||
const adminClient = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
|
||||
)
|
||||
|
||||
// ── 1. Autenticar al llamador ──────────────────────────────────────────────
|
||||
|
||||
const callerToken = req.headers.get('Authorization')?.replace('Bearer ', '')
|
||||
if (!callerToken) {
|
||||
return new Response(JSON.stringify({ error: 'Authorization header requerido' }), { status: 401, headers })
|
||||
}
|
||||
|
||||
const { data: { user: caller }, error: authError } = await adminClient.auth.getUser(callerToken)
|
||||
if (authError || !caller) {
|
||||
return new Response(JSON.stringify({ error: 'Token inválido o expirado' }), { status: 401, headers })
|
||||
}
|
||||
|
||||
// Verificar platform_role desde DB — el JWT no es fuente confiable para el rol
|
||||
const { data: callerProfile, error: profileError } = await adminClient
|
||||
.from('users')
|
||||
.select('platform_role')
|
||||
.eq('id', caller.id)
|
||||
.single()
|
||||
|
||||
if (profileError || callerProfile?.platform_role !== 'platform_admin') {
|
||||
return new Response(JSON.stringify({ error: 'Forbidden: solo platform_admin puede invitar usuarios' }), { status: 403, headers })
|
||||
}
|
||||
|
||||
// ── 2. Parsear y validar el body ──────────────────────────────────────────
|
||||
|
||||
let body: InviteUserRequest
|
||||
try {
|
||||
body = await req.json() as InviteUserRequest
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: 'Body JSON inválido' }), { status: 400, headers })
|
||||
}
|
||||
|
||||
const { email, name, platform_role, org_name } = body
|
||||
|
||||
if (!email || !EMAIL_REGEX.test(email)) {
|
||||
return new Response(JSON.stringify({ error: 'Email inválido o no proporcionado' }), { status: 400, headers })
|
||||
}
|
||||
|
||||
if (!name?.trim()) {
|
||||
return new Response(JSON.stringify({ error: 'El campo name es requerido' }), { status: 400, headers })
|
||||
}
|
||||
|
||||
if (!platform_role || !ALLOWED_ROLES.includes(platform_role)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `platform_role debe ser uno de: ${ALLOWED_ROLES.join(', ')}` }),
|
||||
{ status: 400, headers },
|
||||
)
|
||||
}
|
||||
|
||||
if ((platform_role === 'client_editor' || platform_role === 'client_viewer') && !org_name?.trim()) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'org_name es requerido para roles client_editor y client_viewer' }),
|
||||
{ status: 400, headers },
|
||||
)
|
||||
}
|
||||
|
||||
// ── 3. Resolver org_id ────────────────────────────────────────────────────
|
||||
|
||||
let orgId: string
|
||||
|
||||
if (platform_role === 'member') {
|
||||
// Nuevo miembro InQuality → la org es siempre InQuality (is_provider = true)
|
||||
const { data: inqOrg, error: inqOrgError } = await adminClient
|
||||
.from('organizations')
|
||||
.select('id')
|
||||
.eq('is_provider', true)
|
||||
.single()
|
||||
|
||||
if (inqOrgError || !inqOrg) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No se encontró la organización InQuality en la base de datos' }),
|
||||
{ status: 500, headers },
|
||||
)
|
||||
}
|
||||
orgId = inqOrg.id
|
||||
} else {
|
||||
// Usuario cliente → buscar org por nombre exacto (trim), crear si no existe
|
||||
const trimmedOrgName = org_name!.trim()
|
||||
|
||||
const { data: existingOrg } = await adminClient
|
||||
.from('organizations')
|
||||
.select('id')
|
||||
.eq('name', trimmedOrgName)
|
||||
.single()
|
||||
|
||||
if (existingOrg) {
|
||||
orgId = existingOrg.id
|
||||
} else {
|
||||
const { data: newOrg, error: createOrgError } = await adminClient
|
||||
.from('organizations')
|
||||
.insert({ name: trimmedOrgName, is_provider: false })
|
||||
.select('id')
|
||||
.single()
|
||||
|
||||
if (createOrgError || !newOrg) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Error al crear organización: ${createOrgError?.message ?? 'desconocido'}` }),
|
||||
{ status: 500, headers },
|
||||
)
|
||||
}
|
||||
orgId = newOrg.id
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. Invitar usuario vía Auth Admin API ─────────────────────────────────
|
||||
// inviteUserByEmail crea el usuario en auth.users y envía el email de bienvenida.
|
||||
// Si el email ya existe en auth.users, retorna un error descriptivo.
|
||||
|
||||
const { data: inviteData, error: inviteError } = await adminClient.auth.admin.inviteUserByEmail(
|
||||
email,
|
||||
{
|
||||
data: { full_name: name.trim() }, // se almacena en raw_user_meta_data
|
||||
redirectTo: `${Deno.env.get('SITE_URL') ?? ''}/auth/callback`,
|
||||
},
|
||||
)
|
||||
|
||||
if (inviteError) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: inviteError.message }),
|
||||
{ status: 400, headers },
|
||||
)
|
||||
}
|
||||
|
||||
// ── 5. Crear fila en public.users ─────────────────────────────────────────
|
||||
// UPSERT con ignoreDuplicates: false para manejar la race condition con handle_new_user:
|
||||
// si el trigger ya creó la fila (emails @inquality.com.py), actualizar platform_role y org_id.
|
||||
|
||||
const { error: upsertError } = await adminClient
|
||||
.from('users')
|
||||
.upsert(
|
||||
{
|
||||
id: inviteData.user.id,
|
||||
name: name.trim(),
|
||||
platform_role,
|
||||
org_id: orgId,
|
||||
},
|
||||
{ onConflict: 'id', ignoreDuplicates: false },
|
||||
)
|
||||
|
||||
if (upsertError) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Error al crear perfil de usuario: ${upsertError.message}` }),
|
||||
{ status: 500, headers },
|
||||
)
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, userId: inviteData.user.id }),
|
||||
{ status: 200, headers },
|
||||
)
|
||||
})
|
||||
37
supabase/functions/main/index.ts
Normal file
37
supabase/functions/main/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Edge Function: main (relay)
|
||||
* Enruta todas las requests al worker de la función correspondiente.
|
||||
* Requerido por supabase/edge-runtime en self-hosted (--main-service).
|
||||
* No expuesto directamente — solo opera como router interno.
|
||||
*/
|
||||
|
||||
const FUNCTIONS = ['invite-user', 'revoke-user']
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const url = new URL(req.url)
|
||||
|
||||
// Extraer nombre de función del path: /functions/v1/invite-user → invite-user
|
||||
const segments = url.pathname.split('/').filter(Boolean)
|
||||
const functionName = segments[segments.length - 1]
|
||||
|
||||
if (!functionName || !FUNCTIONS.includes(functionName)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Función no encontrada: ${functionName}` }),
|
||||
{ status: 404, headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}
|
||||
|
||||
const servicePath = `/home/deno/functions/${functionName}`
|
||||
|
||||
try {
|
||||
// EdgeRuntime.userWorkers es el API del supabase/edge-runtime para crear workers por función
|
||||
const worker = await EdgeRuntime.userWorkers.create({ servicePath })
|
||||
return await worker.fetch(req)
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
return new Response(
|
||||
JSON.stringify({ error: message }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } },
|
||||
)
|
||||
}
|
||||
})
|
||||
118
supabase/functions/revoke-user/index.ts
Normal file
118
supabase/functions/revoke-user/index.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Edge Function: revoke-user
|
||||
* Propósito: revocar acceso de un usuario (suspensión suave — cambia platform_role a 'guest')
|
||||
* Solo accesible por platform_admin. No elimina datos ni el usuario de auth.users.
|
||||
* Variables de entorno requeridas: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SITE_URL
|
||||
* - SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY: disponibles automáticamente en Edge Functions
|
||||
* - SITE_URL: agregar manualmente en Supabase Dashboard → Edge Functions → Secrets
|
||||
* Valor producción: https://inq-roi.inqualityhq.com
|
||||
* Deploy: supabase functions deploy revoke-user
|
||||
*/
|
||||
|
||||
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||
|
||||
interface RevokeUserRequest {
|
||||
userId: string
|
||||
}
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': Deno.env.get('SITE_URL') ?? '*',
|
||||
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||
}
|
||||
|
||||
// Manejar CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response('ok', { headers })
|
||||
}
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
return new Response(JSON.stringify({ error: 'Método no permitido' }), { status: 405, headers })
|
||||
}
|
||||
|
||||
const adminClient = createClient(
|
||||
Deno.env.get('SUPABASE_URL')!,
|
||||
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
|
||||
)
|
||||
|
||||
// ── 1. Autenticar al llamador ──────────────────────────────────────────────
|
||||
|
||||
const callerToken = req.headers.get('Authorization')?.replace('Bearer ', '')
|
||||
if (!callerToken) {
|
||||
return new Response(JSON.stringify({ error: 'Authorization header requerido' }), { status: 401, headers })
|
||||
}
|
||||
|
||||
const { data: { user: caller }, error: authError } = await adminClient.auth.getUser(callerToken)
|
||||
if (authError || !caller) {
|
||||
return new Response(JSON.stringify({ error: 'Token inválido o expirado' }), { status: 401, headers })
|
||||
}
|
||||
|
||||
// Verificar platform_role desde DB — el JWT no es fuente confiable para el rol
|
||||
const { data: callerProfile, error: profileError } = await adminClient
|
||||
.from('users')
|
||||
.select('platform_role')
|
||||
.eq('id', caller.id)
|
||||
.single()
|
||||
|
||||
if (profileError || callerProfile?.platform_role !== 'platform_admin') {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Forbidden: solo platform_admin puede revocar accesos' }),
|
||||
{ status: 403, headers },
|
||||
)
|
||||
}
|
||||
|
||||
// ── 2. Parsear y validar el body ──────────────────────────────────────────
|
||||
|
||||
let body: RevokeUserRequest
|
||||
try {
|
||||
body = await req.json() as RevokeUserRequest
|
||||
} catch {
|
||||
return new Response(JSON.stringify({ error: 'Body JSON inválido' }), { status: 400, headers })
|
||||
}
|
||||
|
||||
const { userId } = body
|
||||
|
||||
if (!userId?.trim()) {
|
||||
return new Response(JSON.stringify({ error: 'El campo userId es requerido' }), { status: 400, headers })
|
||||
}
|
||||
|
||||
// ── 3. Prevenir auto-revocación ───────────────────────────────────────────
|
||||
|
||||
if (userId === caller.id) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No puedes revocar tu propio acceso' }),
|
||||
{ status: 400, headers },
|
||||
)
|
||||
}
|
||||
|
||||
// ── 4. Cambiar platform_role a 'guest' ────────────────────────────────────
|
||||
// Suspensión suave: el usuario queda en auth.users pero RLS bloquea acceso a datos.
|
||||
|
||||
const { error: updateError } = await adminClient
|
||||
.from('users')
|
||||
.update({ platform_role: 'guest' })
|
||||
.eq('id', userId)
|
||||
|
||||
if (updateError) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Error al revocar acceso: ${updateError.message}` }),
|
||||
{ status: 500, headers },
|
||||
)
|
||||
}
|
||||
|
||||
// ── 5. Revocar sesiones activas (opcional) ────────────────────────────────
|
||||
// Intenta cerrar las sesiones activas del usuario. Si el método no está disponible
|
||||
// en esta versión del cliente, el rol 'guest' ya impide el acceso via RLS.
|
||||
|
||||
try {
|
||||
await adminClient.auth.admin.signOut(userId)
|
||||
} catch {
|
||||
// No crítico: el rol ya fue cambiado. Las sesiones expirarán naturalmente.
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ status: 200, headers },
|
||||
)
|
||||
})
|
||||
58
supabase/migrations/014_create_organizations.sql
Normal file
58
supabase/migrations/014_create_organizations.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- Tabla de organizaciones clientes
|
||||
-- is_provider = true identifica a InQuality (org administradora de la plataforma)
|
||||
CREATE TABLE IF NOT EXISTS public.organizations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
is_provider boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
ALTER TABLE public.organizations ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Constraint único en name — requerido para ON CONFLICT (name) DO NOTHING en los INSERTs
|
||||
-- siguientes. Debe estar ANTES de cualquier INSERT (orden crítico).
|
||||
ALTER TABLE public.organizations ADD CONSTRAINT organizations_name_unique UNIQUE (name);
|
||||
|
||||
-- Insertar la organización de InQuality como proveedor
|
||||
-- ON CONFLICT DO NOTHING: idempotente si ya existe
|
||||
INSERT INTO public.organizations (name, is_provider)
|
||||
VALUES ('InQuality', true)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Agregar org_id a users (nullable — usuarios InQuality existentes no tienen org de cliente)
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN IF NOT EXISTS org_id uuid REFERENCES public.organizations(id) ON DELETE SET NULL;
|
||||
|
||||
-- Agregar org_id a processes (nullable — procesos existentes se vinculan por migración automática)
|
||||
ALTER TABLE public.processes
|
||||
ADD COLUMN IF NOT EXISTS org_id uuid REFERENCES public.organizations(id) ON DELETE SET NULL;
|
||||
|
||||
-- Ampliar check constraint de platform_role para incluir roles de cliente
|
||||
-- Primero eliminar la restricción existente, luego recrear con los valores nuevos
|
||||
ALTER TABLE public.users DROP CONSTRAINT IF EXISTS users_platform_role_check;
|
||||
ALTER TABLE public.users ADD CONSTRAINT users_platform_role_check
|
||||
CHECK (platform_role IN ('platform_admin', 'member', 'client_editor', 'client_viewer', 'guest'));
|
||||
|
||||
-- ── Migración automática: crear orgs desde valores únicos de processes.client ──
|
||||
-- Crea una organización por cada client name único y no vacío.
|
||||
-- ON CONFLICT (name) DO NOTHING requiere el constraint organizations_name_unique definido arriba.
|
||||
INSERT INTO public.organizations (name, is_provider)
|
||||
SELECT DISTINCT trim(client), false
|
||||
FROM public.processes
|
||||
WHERE client IS NOT NULL AND trim(client) != ''
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Vincular procesos a sus orgs recién creadas
|
||||
UPDATE public.processes p
|
||||
SET org_id = o.id
|
||||
FROM public.organizations o
|
||||
WHERE trim(o.name) = trim(p.client)
|
||||
AND o.is_provider = false
|
||||
AND p.org_id IS NULL;
|
||||
|
||||
-- Asignar org_id de InQuality a usuarios InQuality existentes
|
||||
-- (los que tienen platform_role = 'platform_admin' o 'member')
|
||||
UPDATE public.users u
|
||||
SET org_id = (SELECT id FROM public.organizations WHERE is_provider = true LIMIT 1)
|
||||
WHERE u.org_id IS NULL
|
||||
AND u.platform_role IN ('platform_admin', 'member');
|
||||
293
supabase/migrations/015_client_roles_and_rls.sql
Normal file
293
supabase/migrations/015_client_roles_and_rls.sql
Normal file
@@ -0,0 +1,293 @@
|
||||
-- ── SECURITY DEFINER helpers ──────────────────────────────────────────────────
|
||||
|
||||
-- Retorna el org_id del usuario actual (null para usuarios sin org asignada)
|
||||
CREATE OR REPLACE FUNCTION public.current_org()
|
||||
RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER AS $$
|
||||
SELECT org_id FROM public.users WHERE id = auth.uid()
|
||||
$$;
|
||||
|
||||
-- Retorna true si el usuario pertenece a la organización proveedora (InQuality)
|
||||
CREATE OR REPLACE FUNCTION public.is_inquality()
|
||||
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.users u
|
||||
JOIN public.organizations o ON o.id = u.org_id
|
||||
WHERE u.id = auth.uid() AND o.is_provider = true
|
||||
)
|
||||
$$;
|
||||
|
||||
-- Retorna el platform_role del usuario actual
|
||||
CREATE OR REPLACE FUNCTION public.current_platform_role()
|
||||
RETURNS text LANGUAGE sql STABLE SECURITY DEFINER AS $$
|
||||
SELECT platform_role FROM public.users WHERE id = auth.uid()
|
||||
$$;
|
||||
|
||||
-- ── Trigger handle_new_user ───────────────────────────────────────────────────
|
||||
-- Auto-crea fila en public.users para usuarios @inquality.com.py que se loguean
|
||||
-- con Google OAuth. Los usuarios clientes se crean via Edge Function (invite),
|
||||
-- no por este trigger.
|
||||
-- ON CONFLICT (id) DO NOTHING: no sobrescribir si la Edge Function ya creó la fila.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER AS $$
|
||||
DECLARE
|
||||
provider_org_id uuid;
|
||||
BEGIN
|
||||
IF new.email LIKE '%@inquality.com.py' THEN
|
||||
SELECT id INTO provider_org_id
|
||||
FROM public.organizations
|
||||
WHERE is_provider = true
|
||||
LIMIT 1;
|
||||
|
||||
INSERT INTO public.users (id, name, avatar_url, platform_role, org_id)
|
||||
VALUES (
|
||||
new.id,
|
||||
COALESCE(new.raw_user_meta_data->>'full_name', new.email),
|
||||
new.raw_user_meta_data->>'avatar_url',
|
||||
'member',
|
||||
provider_org_id
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
END IF;
|
||||
RETURN new;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Crear trigger (DROP IF EXISTS para idempotencia)
|
||||
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||
CREATE TRIGGER on_auth_user_created
|
||||
AFTER INSERT ON auth.users
|
||||
FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();
|
||||
|
||||
-- ── RLS: organizations ────────────────────────────────────────────────────────
|
||||
|
||||
CREATE POLICY "orgs_select" ON public.organizations
|
||||
FOR SELECT USING (
|
||||
public.is_inquality()
|
||||
OR id = public.current_org()
|
||||
);
|
||||
|
||||
CREATE POLICY "orgs_insert" ON public.organizations
|
||||
FOR INSERT WITH CHECK (public.is_platform_admin());
|
||||
|
||||
CREATE POLICY "orgs_update" ON public.organizations
|
||||
FOR UPDATE USING (public.is_platform_admin());
|
||||
|
||||
CREATE POLICY "orgs_delete" ON public.organizations
|
||||
FOR DELETE USING (public.is_platform_admin());
|
||||
|
||||
-- ── RLS: users ───────────────────────────────────────────────────────────────
|
||||
-- Reemplazar la política permisiva "read_users" (migration 001) con políticas granulares
|
||||
|
||||
DROP POLICY IF EXISTS "read_users" ON public.users;
|
||||
|
||||
CREATE POLICY "users_select_own" ON public.users
|
||||
FOR SELECT USING (id = auth.uid());
|
||||
|
||||
CREATE POLICY "users_select_admin" ON public.users
|
||||
FOR SELECT USING (public.is_platform_admin());
|
||||
|
||||
-- ── RLS: processes ────────────────────────────────────────────────────────────
|
||||
-- Reemplazar las tres políticas de migration 012 con versiones que incluyen acceso por org
|
||||
|
||||
DROP POLICY IF EXISTS "select_processes" ON public.processes;
|
||||
|
||||
CREATE POLICY "select_processes" ON public.processes
|
||||
FOR SELECT USING (
|
||||
public.is_platform_admin()
|
||||
OR owner_id = auth.uid()
|
||||
OR (
|
||||
public.current_org() IS NOT NULL
|
||||
AND org_id = public.current_org()
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM public.process_access
|
||||
WHERE process_id = processes.id
|
||||
AND (
|
||||
(grantee_type = 'user' AND grantee_id = auth.uid())
|
||||
OR (grantee_type = 'group' AND grantee_id IN (
|
||||
SELECT group_id FROM public.group_memberships WHERE user_id = auth.uid()
|
||||
))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "insert_processes" ON public.processes;
|
||||
|
||||
CREATE POLICY "insert_processes" ON public.processes
|
||||
FOR INSERT WITH CHECK (
|
||||
public.is_platform_admin()
|
||||
OR public.current_platform_role() = 'member'
|
||||
-- client_editor y client_viewer NO pueden crear procesos nuevos
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS "update_processes" ON public.processes;
|
||||
|
||||
CREATE POLICY "update_processes" ON public.processes
|
||||
FOR UPDATE USING (
|
||||
public.is_platform_admin()
|
||||
OR owner_id = auth.uid()
|
||||
OR (
|
||||
public.current_platform_role() = 'client_editor'
|
||||
AND org_id = public.current_org()
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM public.process_access
|
||||
WHERE process_id = processes.id AND permission = 'edit'
|
||||
AND (
|
||||
(grantee_type = 'user' AND grantee_id = auth.uid())
|
||||
OR (grantee_type = 'group' AND grantee_id IN (
|
||||
SELECT group_id FROM public.group_memberships WHERE user_id = auth.uid()
|
||||
))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- ── RLS: activities ──────────────────────────────────────────────────────────
|
||||
-- Reemplazar "all_activities" (migration 006) con políticas granulares por rol
|
||||
|
||||
DROP POLICY IF EXISTS "all_activities" ON public.activities;
|
||||
|
||||
-- SELECT: acceso transitivo — si el usuario puede ver el proceso padre (via processes SELECT policy),
|
||||
-- puede ver sus actividades. El EXISTS aplica RLS de processes automáticamente.
|
||||
CREATE POLICY "activities_select" ON public.activities
|
||||
FOR SELECT USING (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = activities.process_id)
|
||||
);
|
||||
|
||||
-- INSERT / UPDATE: InQuality + client_editor pueden escribir; client_viewer y guest no
|
||||
CREATE POLICY "activities_insert" ON public.activities
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = activities.process_id)
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
CREATE POLICY "activities_update" ON public.activities
|
||||
FOR UPDATE USING (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = activities.process_id)
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
-- DELETE: solo InQuality (platform_admin o member)
|
||||
CREATE POLICY "activities_delete" ON public.activities
|
||||
FOR DELETE USING (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = activities.process_id)
|
||||
AND public.current_platform_role() IN ('platform_admin', 'member')
|
||||
);
|
||||
|
||||
-- ── RLS: resources ────────────────────────────────────────────────────────────
|
||||
-- Reemplazar "all_resources" (migration 005) con políticas granulares por rol.
|
||||
-- NOTA: la tabla resources no tiene columna process_id en este schema — los recursos
|
||||
-- son catálogo global. La policy SELECT mantiene acceso a todos los usuarios autenticados
|
||||
-- (equivalente a la política original) y se alinea con supabaseResourceRepo.getAll().
|
||||
|
||||
DROP POLICY IF EXISTS "all_resources" ON public.resources;
|
||||
|
||||
CREATE POLICY "resources_select" ON public.resources
|
||||
FOR SELECT USING (auth.uid() IS NOT NULL);
|
||||
|
||||
CREATE POLICY "resources_insert" ON public.resources
|
||||
FOR INSERT WITH CHECK (
|
||||
auth.uid() IS NOT NULL
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
CREATE POLICY "resources_update" ON public.resources
|
||||
FOR UPDATE USING (
|
||||
auth.uid() IS NOT NULL
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
CREATE POLICY "resources_delete" ON public.resources
|
||||
FOR DELETE USING (
|
||||
public.current_platform_role() IN ('platform_admin', 'member')
|
||||
);
|
||||
|
||||
-- ── RLS: simulations ─────────────────────────────────────────────────────────
|
||||
-- Reemplazar "all_simulations" (migration 009) con políticas granulares por rol
|
||||
|
||||
DROP POLICY IF EXISTS "all_simulations" ON public.simulations;
|
||||
|
||||
CREATE POLICY "simulations_select" ON public.simulations
|
||||
FOR SELECT USING (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = simulations.process_id)
|
||||
);
|
||||
|
||||
CREATE POLICY "simulations_insert" ON public.simulations
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = simulations.process_id)
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
CREATE POLICY "simulations_delete" ON public.simulations
|
||||
FOR DELETE USING (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = simulations.process_id)
|
||||
AND public.current_platform_role() IN ('platform_admin', 'member')
|
||||
);
|
||||
|
||||
-- ── RLS: activity_resource_assignments ───────────────────────────────────────
|
||||
-- Reemplazar "all_assignments" (migration 007 — nombre real, no "all_activity_resource_assignments")
|
||||
-- con políticas granulares. Heredan acceso del proceso via actividad padre.
|
||||
|
||||
DROP POLICY IF EXISTS "all_assignments" ON public.activity_resource_assignments;
|
||||
|
||||
CREATE POLICY "ara_select" ON public.activity_resource_assignments
|
||||
FOR SELECT USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.activities a
|
||||
JOIN public.processes p ON p.id = a.process_id
|
||||
WHERE a.id = activity_resource_assignments.activity_id
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "ara_write" ON public.activity_resource_assignments
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.activities a
|
||||
JOIN public.processes p ON p.id = a.process_id
|
||||
WHERE a.id = activity_resource_assignments.activity_id
|
||||
)
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
CREATE POLICY "ara_update" ON public.activity_resource_assignments
|
||||
FOR UPDATE USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.activities a
|
||||
JOIN public.processes p ON p.id = a.process_id
|
||||
WHERE a.id = activity_resource_assignments.activity_id
|
||||
)
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
CREATE POLICY "ara_delete" ON public.activity_resource_assignments
|
||||
FOR DELETE USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.activities a
|
||||
JOIN public.processes p ON p.id = a.process_id
|
||||
WHERE a.id = activity_resource_assignments.activity_id
|
||||
)
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
-- ── RLS: gateways ────────────────────────────────────────────────────────────
|
||||
-- Reemplazar "all_gateways" (migration 008) con políticas granulares por rol
|
||||
|
||||
DROP POLICY IF EXISTS "all_gateways" ON public.gateways;
|
||||
|
||||
CREATE POLICY "gateways_select" ON public.gateways
|
||||
FOR SELECT USING (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = gateways.process_id)
|
||||
);
|
||||
|
||||
CREATE POLICY "gateways_write" ON public.gateways
|
||||
FOR INSERT WITH CHECK (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = gateways.process_id)
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
|
||||
CREATE POLICY "gateways_update" ON public.gateways
|
||||
FOR UPDATE USING (
|
||||
EXISTS (SELECT 1 FROM public.processes WHERE id = gateways.process_id)
|
||||
AND public.current_platform_role() NOT IN ('client_viewer', 'guest')
|
||||
);
|
||||
Reference in New Issue
Block a user