Agrego carpeta de supabase
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
|
||||
# dotenvx
|
||||
.env.keys
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -0,0 +1,408 @@
|
||||
# For detailed configuration reference documentation, visit:
|
||||
# https://supabase.com/docs/guides/local-development/cli/config
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "IntegrationWorkshop"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. `public` and `graphql_public` schemas are included by default.
|
||||
schemas = ["public", "graphql_public"]
|
||||
# Extra schemas to add to the search_path of every request.
|
||||
extra_search_path = ["public", "extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[api.tls]
|
||||
# Enable HTTPS endpoints locally using a self-signed certificate.
|
||||
enabled = false
|
||||
# Paths to self-signed certificate pair.
|
||||
# cert_path = "../certs/my-cert.pem"
|
||||
# key_path = "../certs/my-key.pem"
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
# Maximum amount of time to wait for health check when starting the local database.
|
||||
health_timeout = "2m"
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 17
|
||||
|
||||
[db.pooler]
|
||||
enabled = false
|
||||
# Port to use for the local connection pooler.
|
||||
port = 54329
|
||||
# Specifies when a server connection can be reused by other clients.
|
||||
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||
pool_mode = "transaction"
|
||||
# How many server connections to allow per user/database pair.
|
||||
default_pool_size = 20
|
||||
# Maximum number of client connections allowed.
|
||||
max_client_conn = 100
|
||||
|
||||
# [db.vault]
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[db.migrations]
|
||||
# If disabled, migrations will be skipped during a db push or reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of schema files that describe your database.
|
||||
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
|
||||
schema_paths = []
|
||||
|
||||
[db.seed]
|
||||
# If enabled, seeds the database after migrations during a db reset.
|
||||
enabled = true
|
||||
# Specifies an ordered list of seed files to load during db reset.
|
||||
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
|
||||
sql_paths = ["./seed.sql"]
|
||||
|
||||
[db.network_restrictions]
|
||||
# Enable management of network restrictions.
|
||||
enabled = false
|
||||
# List of IPv4 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs = ["0.0.0.0/0"]
|
||||
# List of IPv6 CIDR blocks allowed to connect to the database.
|
||||
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
|
||||
allowed_cidrs_v6 = ["::/0"]
|
||||
|
||||
# Uncomment to reject non-secure connections to the database.
|
||||
# [db.ssl_enforcement]
|
||||
# enabled = true
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||
# ip_version = "IPv6"
|
||||
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||
# max_header_length = 4096
|
||||
|
||||
[studio]
|
||||
enabled = true
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
# External URL of the API server that frontend connects to.
|
||||
api_url = "http://127.0.0.1"
|
||||
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||
openai_api_key = "env(OPENAI_API_KEY)"
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
[storage]
|
||||
enabled = true
|
||||
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||
file_size_limit = "50MiB"
|
||||
|
||||
# Uncomment to configure local storage buckets
|
||||
# [storage.buckets.images]
|
||||
# public = false
|
||||
# file_size_limit = "50MiB"
|
||||
# allowed_mime_types = ["image/png", "image/jpeg"]
|
||||
# objects_path = "./images"
|
||||
|
||||
# Allow connections via S3 compatible clients
|
||||
[storage.s3_protocol]
|
||||
enabled = true
|
||||
|
||||
# Image transformation API is available to Supabase Pro plan.
|
||||
# [storage.image_transformation]
|
||||
# enabled = true
|
||||
|
||||
# Store analytical data in S3 for running ETL jobs over Iceberg Catalog
|
||||
# This feature is only available on the hosted platform.
|
||||
[storage.analytics]
|
||||
enabled = false
|
||||
max_namespaces = 5
|
||||
max_tables = 10
|
||||
max_catalogs = 2
|
||||
|
||||
# Analytics Buckets is available to Supabase Pro plan.
|
||||
# [storage.analytics.buckets.my-warehouse]
|
||||
|
||||
# Store vector embeddings in S3 for large and durable datasets
|
||||
# This feature is only available on the hosted platform.
|
||||
[storage.vector]
|
||||
enabled = false
|
||||
max_buckets = 10
|
||||
max_indexes = 5
|
||||
|
||||
# Vector Buckets is available to Supabase Pro plan.
|
||||
# [storage.vector.buckets.documents-openai]
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://127.0.0.1:3000"
|
||||
# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended.
|
||||
# external_url = ""
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# JWT issuer URL. If not set, defaults to auth.external_url.
|
||||
# jwt_issuer = ""
|
||||
# Path to JWT signing key. DO NOT commit your signing keys file to git.
|
||||
# signing_keys_path = "./signing_keys.json"
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
# Requires enable_refresh_token_rotation = true.
|
||||
refresh_token_reuse_interval = 10
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
# Allow/disallow anonymous sign-ins to your project.
|
||||
enable_anonymous_sign_ins = false
|
||||
# Allow/disallow testing manual linking of accounts
|
||||
enable_manual_linking = false
|
||||
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
|
||||
minimum_password_length = 6
|
||||
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
|
||||
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
|
||||
password_requirements = ""
|
||||
|
||||
# Configure passkey sign-ins.
|
||||
# [auth.passkey]
|
||||
# enabled = false
|
||||
|
||||
# Configure WebAuthn relying party settings (required when passkey is enabled).
|
||||
# [auth.webauthn]
|
||||
# rp_display_name = "Supabase"
|
||||
# rp_id = "localhost"
|
||||
# rp_origins = ["http://127.0.0.1:3000"]
|
||||
|
||||
[auth.rate_limit]
|
||||
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
|
||||
email_sent = 2
|
||||
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
|
||||
sms_sent = 30
|
||||
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
|
||||
anonymous_users = 30
|
||||
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
|
||||
token_refresh = 150
|
||||
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
|
||||
sign_in_sign_ups = 30
|
||||
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
|
||||
token_verifications = 30
|
||||
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
|
||||
web3 = 30
|
||||
|
||||
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
|
||||
# [auth.captcha]
|
||||
# enabled = true
|
||||
# provider = "hcaptcha"
|
||||
# secret = ""
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = false
|
||||
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
|
||||
secure_password_change = false
|
||||
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||
max_frequency = "1s"
|
||||
# Number of characters used in the email OTP.
|
||||
otp_length = 6
|
||||
# Number of seconds before the email OTP expires (defaults to 1 hour).
|
||||
otp_expiry = 3600
|
||||
|
||||
# Use a production-ready SMTP server
|
||||
# [auth.email.smtp]
|
||||
# enabled = true
|
||||
# host = "smtp.sendgrid.net"
|
||||
# port = 587
|
||||
# user = "apikey"
|
||||
# pass = "env(SENDGRID_API_KEY)"
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
# Uncomment to customize email template
|
||||
# [auth.email.template.invite]
|
||||
# subject = "You have been invited"
|
||||
# content_path = "./supabase/templates/invite.html"
|
||||
|
||||
# Uncomment to customize notification email template
|
||||
# [auth.email.notification.password_changed]
|
||||
# enabled = true
|
||||
# subject = "Your password has been changed"
|
||||
# content_path = "./templates/password_changed_notification.html"
|
||||
|
||||
[auth.sms]
|
||||
# Allow/disallow new user signups via SMS to your project.
|
||||
enable_signup = false
|
||||
# If enabled, users need to confirm their phone number before signing in.
|
||||
enable_confirmations = false
|
||||
# Template for sending OTP to users
|
||||
template = "Your code is {{ .Code }}"
|
||||
# Controls the minimum amount of time that must pass before sending another sms otp.
|
||||
max_frequency = "5s"
|
||||
|
||||
# Use pre-defined map of phone number to OTP for testing.
|
||||
# [auth.sms.test_otp]
|
||||
# 4152127777 = "123456"
|
||||
|
||||
# Configure logged in session timeouts.
|
||||
# [auth.sessions]
|
||||
# Force log out after the specified duration.
|
||||
# timebox = "24h"
|
||||
# Force log out if the user has been inactive longer than the specified duration.
|
||||
# inactivity_timeout = "8h"
|
||||
|
||||
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
|
||||
# [auth.hook.before_user_created]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://postgres/auth/before-user-created-hook"
|
||||
|
||||
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||
# [auth.hook.custom_access_token]
|
||||
# enabled = true
|
||||
# uri = "pg-functions://<database>/<schema>/<hook_name>"
|
||||
|
||||
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
||||
[auth.sms.twilio]
|
||||
enabled = false
|
||||
account_sid = ""
|
||||
message_service_sid = ""
|
||||
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||
|
||||
# Multi-factor-authentication is available to Supabase Pro plan.
|
||||
[auth.mfa]
|
||||
# Control how many MFA factors can be enrolled at once per user.
|
||||
max_enrolled_factors = 10
|
||||
|
||||
# Control MFA via App Authenticator (TOTP)
|
||||
[auth.mfa.totp]
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
|
||||
# Configure MFA via Phone Messaging
|
||||
[auth.mfa.phone]
|
||||
enroll_enabled = false
|
||||
verify_enabled = false
|
||||
otp_length = 6
|
||||
template = "Your code is {{ .Code }}"
|
||||
max_frequency = "5s"
|
||||
|
||||
# Configure MFA via WebAuthn
|
||||
# [auth.mfa.web_authn]
|
||||
# enroll_enabled = true
|
||||
# verify_enabled = true
|
||||
|
||||
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||
# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`.
|
||||
[auth.external.apple]
|
||||
enabled = false
|
||||
client_id = ""
|
||||
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
||||
# Overrides the default auth callback URL derived from auth.external_url.
|
||||
redirect_uri = ""
|
||||
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||
# or any other third-party OIDC providers.
|
||||
url = ""
|
||||
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||
skip_nonce_check = false
|
||||
# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address.
|
||||
email_optional = false
|
||||
|
||||
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
|
||||
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
|
||||
[auth.web3.solana]
|
||||
enabled = false
|
||||
|
||||
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.firebase]
|
||||
enabled = false
|
||||
# project_id = "my-firebase-project"
|
||||
|
||||
# Use Auth0 as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.auth0]
|
||||
enabled = false
|
||||
# tenant = "my-auth0-tenant"
|
||||
# tenant_region = "us"
|
||||
|
||||
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.aws_cognito]
|
||||
enabled = false
|
||||
# user_pool_id = "my-user-pool-id"
|
||||
# user_pool_region = "us-east-1"
|
||||
|
||||
# Use Clerk as a third-party provider alongside Supabase Auth.
|
||||
[auth.third_party.clerk]
|
||||
enabled = false
|
||||
# Obtain from https://clerk.com/setup/supabase
|
||||
# domain = "example.clerk.accounts.dev"
|
||||
|
||||
# OAuth server configuration
|
||||
[auth.oauth_server]
|
||||
# Enable OAuth server functionality
|
||||
enabled = false
|
||||
# Path for OAuth consent flow UI
|
||||
authorization_url_path = "/oauth/consent"
|
||||
# Allow dynamic client registration
|
||||
allow_dynamic_registration = false
|
||||
|
||||
[edge_runtime]
|
||||
enabled = true
|
||||
# Supported request policies: `oneshot`, `per_worker`.
|
||||
# `per_worker` (default) — enables hot reload during local development.
|
||||
# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks).
|
||||
policy = "per_worker"
|
||||
# Port to attach the Chrome inspector for debugging edge functions.
|
||||
inspector_port = 8083
|
||||
# The Deno major version to use.
|
||||
deno_version = 2
|
||||
|
||||
# [edge_runtime.secrets]
|
||||
# secret_key = "env(SECRET_VALUE)"
|
||||
|
||||
[analytics]
|
||||
enabled = true
|
||||
port = 54327
|
||||
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||
backend = "postgres"
|
||||
|
||||
# Experimental features may be deprecated any time
|
||||
[experimental]
|
||||
# Configures Postgres storage engine to use OrioleDB (S3)
|
||||
orioledb_version = ""
|
||||
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
||||
s3_host = "env(S3_HOST)"
|
||||
# Configures S3 bucket region, eg. us-east-1
|
||||
s3_region = "env(S3_REGION)"
|
||||
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
||||
s3_access_key = "env(S3_ACCESS_KEY)"
|
||||
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
||||
s3_secret_key = "env(S3_SECRET_KEY)"
|
||||
|
||||
# [experimental.pgdelta]
|
||||
# When enabled, pg-delta becomes the active engine for supported schema flows.
|
||||
# enabled = false
|
||||
# Directory under `supabase/` where declarative files are written.
|
||||
# declarative_schema_path = "./database"
|
||||
# JSON string passed through to pg-delta SQL formatting.
|
||||
# format_options = "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
-- ============================================================================
|
||||
-- internal.entorno — tabla de identidad del entorno
|
||||
-- ============================================================================
|
||||
-- Migración delta (Carril 1). La base declara si es 'dev' o 'prod'. Es la piedra
|
||||
-- angular de los guardarraíles (ver Documentation/EntornoDesarrollo.md §F): toda
|
||||
-- herramienta que vaya a escribir consulta esta fila antes de operar, y frena si
|
||||
-- la base no se sabe a sí misma.
|
||||
--
|
||||
-- DISEÑO
|
||||
-- - Fila ÚNICA forzada: id boolean PK DEFAULT true + CHECK (id). Solo el valor
|
||||
-- true satisface el CHECK, y al ser PK no puede repetirse → a lo sumo una
|
||||
-- fila, jamás dos identidades en conflicto.
|
||||
-- - valor ∈ {'dev','prod'} por CHECK. No hay tercer estado válido.
|
||||
--
|
||||
-- FAIL-SAFE (deliberado)
|
||||
-- La tabla NO se seedea desde el repo (esta migración no trae INSERT). Una base
|
||||
-- recién clonada nace con la tabla VACÍA. Tabla vacía = identidad desconocida =
|
||||
-- la herramienta se niega a escribir. La identidad se setea a mano por entorno
|
||||
-- como paso explícito post-clon (US-E11 y el checklist de promoción US-E16).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS internal.entorno (
|
||||
id boolean DEFAULT true NOT NULL,
|
||||
valor text NOT NULL,
|
||||
CONSTRAINT entorno_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT entorno_fila_unica CHECK (id),
|
||||
CONSTRAINT entorno_valor_valido CHECK (valor IN ('dev', 'prod'))
|
||||
);
|
||||
|
||||
ALTER TABLE internal.entorno ENABLE ROW LEVEL SECURITY;
|
||||
@@ -0,0 +1,82 @@
|
||||
-- ============================================================================
|
||||
-- seed: internal.app_config (Carril 1 — migración de datos)
|
||||
-- ============================================================================
|
||||
-- Pares clave/valor de configuración runtime. Cada clave es leída por las
|
||||
-- funciones consumidoras vía internal.get_config_int(clave, default); si la
|
||||
-- fila no existe, la función cae al default. Las filas de este seed existen
|
||||
-- para que el operador pueda hacer override sin redeploy y para que la tabla
|
||||
-- documente qué parámetros expone el sistema.
|
||||
--
|
||||
-- ORIGEN: estos 9 valores se verificaron contra prod (xkmaysctkjsytgmxrkyo) el
|
||||
-- 2026-06-04 y coinciden exactos — no hay override operativo en prod, el seed
|
||||
-- es fiel a la realidad. Migración trasladada desde el archivo histórico
|
||||
-- database/schema/seeds/internal/app_config.sql al Carril 1.
|
||||
--
|
||||
-- Convención:
|
||||
-- - Una sección por dominio (retencion.*, pagos.*, etc.).
|
||||
-- - El valor seedeado debe coincidir con el default que el consumidor pasa
|
||||
-- a get_config_int. Si los dos divergen, el seed manda.
|
||||
-- - ON CONFLICT DO NOTHING: idempotente y preserva overrides operativos.
|
||||
-- Promover esta migración a prod (que ya tiene las filas) es un no-op.
|
||||
-- Para reescribir un valor, hacer UPDATE explícito.
|
||||
--
|
||||
-- Stories asociadas:
|
||||
-- - Documentation/Backlog/PoliticaRetencion.md US-R04 (sección retencion.*)
|
||||
-- - Documentation/Backlog/EntornoDesarrollo.md US-E09 (criterio de seed)
|
||||
-- ============================================================================
|
||||
|
||||
-- ─── Retención operativa ────────────────────────────────────────────────────
|
||||
-- Política: Documentation/PoliticaRetencion.md §3 (archivado) y §7 (higiene).
|
||||
-- Unidad: meses. Consumidores: funciones de purga (US-R06..R09), validación
|
||||
-- de registro de pagos (US-R10), y orquestación del pipeline mensual (US-R17).
|
||||
|
||||
INSERT INTO internal.app_config (clave, valor, descripcion) VALUES
|
||||
('retencion.pagos_meses', '12',
|
||||
'Ventana operativa de pagos en meses. Al cumplirse, el pago migra al archivo XLSX y se borra de la base.'),
|
||||
('retencion.turnos_meses', '2',
|
||||
'Antigüedad máxima de turnos pasados antes de purgarse (CASCADE arrastra reservas asociadas).'),
|
||||
('retencion.eventos_pagos_meses', '3',
|
||||
'Antigüedad máxima de eventos de pagos (prefijo pago_) antes de purgarse.'),
|
||||
('retencion.eventos_otros_meses', '2',
|
||||
'Antigüedad máxima del resto de eventos antes de purgarse.'),
|
||||
('retencion.dias_especiales_meses', '1',
|
||||
'Antigüedad máxima de días especiales pasados antes de purgarse (CASCADE arrastra horario_actividad_especial).')
|
||||
ON CONFLICT (clave) DO NOTHING;
|
||||
|
||||
-- ─── Retención: cap de seguridad de eventos ─────────────────────────────────
|
||||
-- Tope duro al tamaño de public.eventos. Unidad: filas (conteo, no meses).
|
||||
-- Consumidor: internal.log_evento (US-R12). No es una ventana temporal: es un
|
||||
-- freno contra un bug que dispare eventos en loop y consuma la cuota de la base
|
||||
-- antes de la próxima purga mensual. Al alcanzarse, log_evento rechaza el insert
|
||||
-- con excepción (no FIFO): el rechazo hace fallar la función que dispara el
|
||||
-- evento y corta la cascada, en vez de borrar eventos legítimos en silencio.
|
||||
|
||||
INSERT INTO internal.app_config (clave, valor, descripcion) VALUES
|
||||
('retencion.eventos_cap', '50000',
|
||||
'Tope de filas en public.eventos. Al alcanzarlo, internal.log_evento rechaza nuevos inserts con excepción (freno anti-loop, no FIFO).')
|
||||
ON CONFLICT (clave) DO NOTHING;
|
||||
|
||||
-- ─── Pagos: ventanas operativas ─────────────────────────────────────────────
|
||||
-- Claves ya consumidas por funciones existentes (fc_editar_pago, fc_anular_pago,
|
||||
-- fc_reservar_turno) que hasta ahora venían cayendo al default por ausencia de
|
||||
-- fila. Sembradas acá para que queden explícitas y editables sin redeploy.
|
||||
|
||||
INSERT INTO internal.app_config (clave, valor, descripcion) VALUES
|
||||
('pagos.ventana_edicion_minutos', '30',
|
||||
'Minutos desde el registro durante los cuales un pago admite edición o anulación sin disparar evento. Leída por fc_editar_pago y fc_anular_pago.'),
|
||||
('pagos.umbral_morosidad_meses', '2',
|
||||
'Cantidad de meses impagos a partir de los cuales se bloquea al cliente para reservar turnos. Leída por fc_reservar_turno.')
|
||||
ON CONFLICT (clave) DO NOTHING;
|
||||
|
||||
-- ─── Retención: huérfanas resueltas (ventana de seguridad) ──────────────────
|
||||
-- Unidad: días (no meses). Consumidor: internal.limpiar_huerfanas_resueltas.
|
||||
-- Es el piso de gracia para deshacer un cambio de estado por error: una
|
||||
-- huérfana en estado terminal (reubicado/resuelta) recién se vuelve elegible
|
||||
-- para purgar cuando pasaron al menos N días desde su `resuelto_en`. La purga
|
||||
-- real corre en la cadencia del cron mensual; en la práctica las terminales
|
||||
-- viven entre N+1 y N+31 días.
|
||||
|
||||
INSERT INTO internal.app_config (clave, valor, descripcion) VALUES
|
||||
('retencion.huerfanas_resueltas_dias', '14',
|
||||
'Días de gracia mínimos que una reserva huérfana en estado terminal (reubicado/resuelta) debe tener desde su cambio de estado para ser elegible para purga.')
|
||||
ON CONFLICT (clave) DO NOTHING;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- Mueve el range type timerange (y su multirange) de public a internal.
|
||||
--
|
||||
-- Motivo: `supabase db reset` vacía el schema public objeto por objeto y no puede
|
||||
-- dropear la función constructora que Postgres genera para un range type, así que el
|
||||
-- reset se traba. En cambio borra los schemas no-public enteros con DROP SCHEMA
|
||||
-- CASCADE, donde el tipo y su constructor se van juntos. Con timerange en internal,
|
||||
-- el reset de dev funciona.
|
||||
--
|
||||
-- Es seguro: ninguna función referencia el tipo; el único objeto que lo usa es la
|
||||
-- constraint de no-solapamiento de horario_actividad, que se recrea acá apuntando a
|
||||
-- internal.timerange. No cambia ningún dato.
|
||||
|
||||
ALTER TABLE "public"."horario_actividad"
|
||||
DROP CONSTRAINT "horario_actividad_no_solapamiento";
|
||||
|
||||
DROP TYPE "public"."timerange" CASCADE; -- arrastra public.timemultirange
|
||||
|
||||
CREATE TYPE "internal"."timerange" AS RANGE (
|
||||
subtype = time without time zone,
|
||||
multirange_type_name = "internal"."timemultirange"
|
||||
);
|
||||
|
||||
ALTER TABLE "public"."horario_actividad"
|
||||
ADD CONSTRAINT "horario_actividad_no_solapamiento"
|
||||
EXCLUDE USING "gist" (
|
||||
"dia_semana" WITH =,
|
||||
"actividad_id" WITH =,
|
||||
"internal"."timerange"("hora_inicio", "hora_fin") WITH &&,
|
||||
"daterange"("valido_desde", COALESCE("valido_hasta", 'infinity'::"date"), '[]'::"text") WITH &&
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Setup del módulo de retención: las extensiones del cron y el job programado.
|
||||
--
|
||||
-- Va como migración para que un `supabase db reset` reconstruya también el cron, no
|
||||
-- solo el esquema. Antes esto se montaba a mano por entorno; al ponerlo acá, dev se
|
||||
-- recupera entero después de un reset sin pasos manuales.
|
||||
--
|
||||
-- Lo que sigue siendo manual y NO puede ir acá:
|
||||
-- - El deploy del Edge Function `pipeline-mensual` (es otra superficie, no vive en
|
||||
-- la base).
|
||||
-- - Los secretos del Vault (`pipeline_mensual_url`, `pipeline_mensual_auth`). Son
|
||||
-- secretos; además sobreviven al reset, así que solo hay que cargarlos a mano en
|
||||
-- un proyecto nuevo o si se borran. El job los lee en runtime, no al programarse.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_cron";
|
||||
|
||||
-- Idempotente: si el job ya existe (prod, o una re-aplicación), lo saca antes de
|
||||
-- volver a programarlo. Si no existe, no devuelve filas y no hace nada.
|
||||
SELECT cron.unschedule(jobid) FROM cron.job WHERE jobname = 'pipeline_mensual_job';
|
||||
|
||||
SELECT cron.schedule(
|
||||
'pipeline_mensual_job',
|
||||
'0 6 1 * *', -- 06:00 UTC = 03:00 AM Argentina (UTC-3, sin DST). Día 1.
|
||||
$job$
|
||||
SELECT net.http_post(
|
||||
url := (SELECT decrypted_secret FROM vault.decrypted_secrets
|
||||
WHERE name = 'pipeline_mensual_url'),
|
||||
headers := jsonb_build_object(
|
||||
'Content-Type', 'application/json',
|
||||
'Authorization', 'Bearer ' || (SELECT decrypted_secret
|
||||
FROM vault.decrypted_secrets
|
||||
WHERE name = 'pipeline_mensual_auth')
|
||||
),
|
||||
body := '{}'::jsonb,
|
||||
timeout_milliseconds := 300000
|
||||
);
|
||||
$job$
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Despliega fc_iniciar_sesion_por_token: reanuda la sesión desde el token guardado,
|
||||
-- extiende su TTL +10 días y devuelve el rol. Reemplaza a fc_validar_token en el
|
||||
-- arranque de la app. Función nueva (nombre nuevo) → CREATE OR REPLACE pelado, sin DROP.
|
||||
-- Fuente canónica: database/functions/auth/fc_iniciar_sesion_por_token.sql
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_iniciar_sesion_por_token(p_token UUID)
|
||||
RETURNS TEXT
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_rol TEXT;
|
||||
BEGIN
|
||||
UPDATE internal.sesiones s
|
||||
SET expires_at = NOW() + INTERVAL '10 days'
|
||||
FROM public.usuarios u
|
||||
WHERE s.token = p_token
|
||||
AND s.expires_at > NOW()
|
||||
AND u.id = s.usuario_id
|
||||
RETURNING u.rol INTO v_rol;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Sesión inválida o expirada.';
|
||||
END IF;
|
||||
|
||||
RETURN v_rol;
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,304 @@
|
||||
-- Feature "cambio de contraseña": password_hash pasa a ser opcional (los
|
||||
-- clientes no inician sesión en este panel, usan el bot de WhatsApp), por lo
|
||||
-- que fc_insertar_usuario ya no exige password cuando rol = 'cliente'.
|
||||
-- fc_modificar_usuario aprende a aceptar un campo opcional 'password' en el
|
||||
-- PATCH (gateado por el permiso dedicado resetear_contrasena_usuario). Se
|
||||
-- agregan dos funciones nuevas: fc_cambiar_propia_contrasena (autoservicio,
|
||||
-- admin/superadmin cambian la suya confirmando la actual) y
|
||||
-- fc_resetear_contrasena_usuario (reset administrativo, solo superadmin,
|
||||
-- sobre cualquier no-cliente, sin pedir la actual del objetivo).
|
||||
-- Fuentes canónicas:
|
||||
-- database/schema/tables/public/usuarios.sql
|
||||
-- database/functions/usuarios/fc_insertar_usuario.sql
|
||||
-- database/functions/usuarios/fc_modificar_usuario.sql
|
||||
-- database/functions/usuarios/fc_cambiar_propia_contrasena.sql (nuevo)
|
||||
-- database/functions/usuarios/fc_resetear_contrasena_usuario.sql (nuevo)
|
||||
|
||||
-- 1. password_hash ya no es NOT NULL
|
||||
ALTER TABLE public.usuarios ALTER COLUMN password_hash DROP NOT NULL;
|
||||
|
||||
-- 2. Permisos nuevos.
|
||||
-- Ids explícitos 43/44 a propósito: en el db reset esta migración corre ANTES
|
||||
-- del seed, que trae los permisos 1..42 con ids fijos. Si dejáramos que el id
|
||||
-- lo genere la identity, en una base recién creada agarraría 1 y 2 y el seed
|
||||
-- chocaría (permisos_pkey). En prod, donde ya existen 1..42, 43/44 quedan libres.
|
||||
INSERT INTO internal.permisos OVERRIDING SYSTEM VALUE VALUES
|
||||
(43, 'cambiar_propia_contrasena', ARRAY['superadmin','admin']),
|
||||
(44, 'resetear_contrasena_usuario', ARRAY['superadmin']);
|
||||
SELECT setval('internal.permisos_id_permiso_seq', 44, true);
|
||||
|
||||
-- 3. fc_insertar_usuario: password obligatorio salvo para rol 'cliente'.
|
||||
-- Misma firma -> CREATE OR REPLACE sin DROP.
|
||||
CREATE OR REPLACE FUNCTION public.fc_insertar_usuario(p_datos jsonb, p_token uuid)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO extensions, public
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_password_hash TEXT;
|
||||
v_rol TEXT;
|
||||
|
||||
-- Campos del nuevo usuario
|
||||
new_dni TEXT;
|
||||
new_nombre TEXT;
|
||||
new_apellido TEXT;
|
||||
new_mail TEXT;
|
||||
new_telefono TEXT;
|
||||
new_peso DECIMAL(5,2);
|
||||
new_altura INTEGER;
|
||||
new_fuerza_max DECIMAL(10,2);
|
||||
new_sexo TEXT;
|
||||
new_rol TEXT;
|
||||
new_password TEXT;
|
||||
new_isactive BOOL;
|
||||
new_tipo_cuota UUID;
|
||||
|
||||
BEGIN
|
||||
-- 1. Validar el token de sesión y obtener el rol del usuario que llama.
|
||||
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'modificar_usuarios');
|
||||
|
||||
-- 2. Validar que no esté intentando crear un admin o superadmin si no puede
|
||||
|
||||
new_rol := p_datos->>'rol';
|
||||
|
||||
IF new_rol = 'superadmin' THEN
|
||||
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'agregar_superadmin');
|
||||
ELSIF new_rol = 'admin' THEN
|
||||
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'agregar_admin');
|
||||
END IF;
|
||||
|
||||
-- 2.5 Extraer campos del JSONB
|
||||
new_dni := p_datos->>'dni';
|
||||
new_nombre := p_datos->>'nombre';
|
||||
new_apellido := p_datos->>'apellido';
|
||||
new_mail := p_datos->>'mail';
|
||||
new_telefono := p_datos->>'telefono';
|
||||
new_peso := (p_datos->>'peso')::DECIMAL;
|
||||
new_altura := (p_datos->>'altura')::INTEGER;
|
||||
new_fuerza_max := (p_datos->>'fuerza_max')::DECIMAL;
|
||||
new_sexo := COALESCE(p_datos->>'sexo', 'Hombre');
|
||||
|
||||
new_password := p_datos->>'password';
|
||||
new_isactive := (p_datos->>'isActive')::BOOLEAN;
|
||||
|
||||
-- CORRECCIÓN 1: Validar que exista la key Y que NO sea null antes de buscar en la BD
|
||||
IF p_datos ? 'tipo_cuota' AND (p_datos->>'tipo_cuota') IS NOT NULL THEN
|
||||
new_tipo_cuota := (p_datos->>'tipo_cuota')::UUID;
|
||||
|
||||
-- Validar que exista el tipo_cuota en la tabla padre
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM tipos_cuota
|
||||
WHERE id = new_tipo_cuota
|
||||
) THEN
|
||||
RAISE EXCEPTION 'El tipo_cuota % no existe.', new_tipo_cuota;
|
||||
END IF;
|
||||
ELSE
|
||||
-- Si no viene o es null, asignamos NULL explícitamente
|
||||
new_tipo_cuota := NULL;
|
||||
END IF;
|
||||
|
||||
-- La contraseña es obligatoria salvo para clientes: no necesitan loguearse
|
||||
-- en este panel (usan el bot de WhatsApp). Si es cliente y no la mandan,
|
||||
-- password_hash queda NULL.
|
||||
IF new_password IS NULL AND new_rol <> 'cliente' THEN
|
||||
RAISE EXCEPTION 'La contraseña es obligatoria';
|
||||
END IF;
|
||||
|
||||
-- 3. Hashear la contraseña solo si vino (clientes pueden no traerla)
|
||||
IF new_password IS NOT NULL THEN
|
||||
SELECT internal.create_password_hash(new_password) INTO v_password_hash;
|
||||
IF v_password_hash IS NULL THEN
|
||||
RAISE EXCEPTION 'Error al generar el hash de la contraseña';
|
||||
END IF;
|
||||
ELSE
|
||||
v_password_hash := NULL;
|
||||
END IF;
|
||||
|
||||
-- 4. Insertar el nuevo usuario
|
||||
-- CORRECCIÓN 2: Agregué 'tipo_cuota' y 'new_tipo_cuota' al INSERT
|
||||
INSERT INTO public.usuarios (
|
||||
dni, nombre, apellido, mail, telefono, peso, altura, fuerza_max,
|
||||
sexo, rol, password_hash, isactive, tipo_cuota
|
||||
) VALUES (
|
||||
new_dni, new_nombre, new_apellido, new_mail, new_telefono, new_peso, new_altura, new_fuerza_max,
|
||||
new_sexo, new_rol, v_password_hash, new_isactive, new_tipo_cuota
|
||||
);
|
||||
END;
|
||||
$function$;
|
||||
|
||||
-- 4. fc_modificar_usuario: acepta password opcional en el PATCH, gateado por
|
||||
-- el permiso dedicado resetear_contrasena_usuario. Misma firma -> CREATE
|
||||
-- OR REPLACE sin DROP.
|
||||
CREATE OR REPLACE FUNCTION public.fc_modificar_usuario(p_datos jsonb, p_token uuid)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO extensions, public
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_isActive_new BOOLEAN := NULL;
|
||||
v_rol_actual TEXT;
|
||||
v_uid UUID;
|
||||
v_password_hash TEXT;
|
||||
BEGIN
|
||||
|
||||
-- Validar token
|
||||
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'modificar_usuarios');
|
||||
|
||||
-- Evaluamos primero si el JSON trae un ID válido
|
||||
IF p_datos ? 'id' AND p_datos->>'id' IS NOT NULL THEN
|
||||
SELECT u.id, u.rol INTO v_uid, v_rol_actual
|
||||
FROM public.usuarios u
|
||||
WHERE u.id = (p_datos->>'id')::UUID
|
||||
FOR UPDATE;
|
||||
|
||||
-- Si no hay ID, intentamos usar el DNI (fallback)
|
||||
ELSIF p_datos ? 'dni' AND p_datos->>'dni' IS NOT NULL THEN
|
||||
SELECT u.id, u.rol INTO v_uid, v_rol_actual
|
||||
FROM public.usuarios u
|
||||
WHERE u.dni = p_datos->>'dni'
|
||||
FOR UPDATE;
|
||||
|
||||
-- Si no mandaron ni ID ni DNI, no sabemos a quién modificar
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Se requiere un ID o un DNI para identificar al usuario.';
|
||||
END IF;
|
||||
|
||||
-- Si hicimos la búsqueda pero no encontramos coincidencias
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Usuario no encontrado con los datos proporcionados.';
|
||||
END IF;
|
||||
|
||||
-- Validar existencia del tipo_cuota SOLO si viene en el JSON y NO es null
|
||||
IF p_datos ? 'tipo_cuota' AND (p_datos->>'tipo_cuota') IS NOT NULL THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM tipos_cuota
|
||||
WHERE id = (p_datos->>'tipo_cuota')::UUID
|
||||
) THEN
|
||||
RAISE EXCEPTION 'El tipo_cuota % no existe.', p_datos->>'tipo_cuota';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Validar si viene un rol en el JSON y si es distinto al que ya está en la DB
|
||||
IF p_datos ? 'rol' AND (p_datos->>'rol') IS DISTINCT FROM v_rol_actual THEN
|
||||
-- Delegamos la responsabilidad a la nueva función
|
||||
PERFORM public.fc_modificar_rol_usuario(v_uid, p_datos->>'rol', p_token);
|
||||
|
||||
-- Si llega hasta acá (no tiró excepción), eliminamos la key 'rol' del json
|
||||
-- para que el UPDATE de abajo no lo sobreescriba innecesariamente.
|
||||
p_datos := p_datos - 'rol';
|
||||
END IF;
|
||||
|
||||
-- Password opcional: solo si viene la key Y no es null/vacía, se re-hashea
|
||||
-- y se agrega al UPDATE. Acción sensible -> permiso dedicado, no alcanza
|
||||
-- con 'modificar_usuarios' (mismo criterio que ya se usa para 'rol').
|
||||
IF p_datos ? 'password' AND COALESCE(p_datos->>'password', '') <> '' THEN
|
||||
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'resetear_contrasena_usuario');
|
||||
v_password_hash := internal.create_password_hash(p_datos->>'password');
|
||||
IF v_password_hash IS NULL THEN
|
||||
RAISE EXCEPTION 'Error al generar el hash de la contraseña';
|
||||
END IF;
|
||||
ELSE
|
||||
v_password_hash := NULL;
|
||||
END IF;
|
||||
|
||||
UPDATE public.usuarios
|
||||
SET
|
||||
nombre = CASE WHEN p_datos ? 'nombre' THEN p_datos->>'nombre' ELSE nombre END,
|
||||
apellido = CASE WHEN p_datos ? 'apellido' THEN p_datos->>'apellido' ELSE apellido END,
|
||||
peso = CASE WHEN p_datos ? 'peso' THEN (p_datos->>'peso')::DECIMAL ELSE peso END,
|
||||
altura = CASE WHEN p_datos ? 'altura' THEN (p_datos->>'altura')::DECIMAL ELSE altura END,
|
||||
mail = CASE WHEN p_datos ? 'mail' THEN p_datos->>'mail' ELSE mail END,
|
||||
telefono = CASE WHEN p_datos ? 'telefono' THEN p_datos->>'telefono' ELSE telefono END,
|
||||
fuerza_max= CASE WHEN p_datos ? 'fuerza_max' THEN (p_datos->>'fuerza_max')::DECIMAL ELSE fuerza_max END,
|
||||
sexo = CASE WHEN p_datos ? 'sexo' THEN p_datos->>'sexo' ELSE sexo END,
|
||||
-- Esto ahora permite cambiar el tipo de cuota a NULL si mandas null, o cambiarlo a otro ID
|
||||
tipo_cuota= CASE WHEN p_datos ? 'tipo_cuota' THEN (p_datos->>'tipo_cuota')::UUID ELSE tipo_cuota END,
|
||||
password_hash = CASE WHEN v_password_hash IS NOT NULL THEN v_password_hash ELSE password_hash END,
|
||||
fecha_modificacion = now()
|
||||
WHERE id = v_uid;
|
||||
|
||||
END;
|
||||
$function$;
|
||||
|
||||
-- 5. fc_cambiar_propia_contrasena: autoservicio (admin/superadmin cambian la
|
||||
-- suya, confirmando la actual). Función nueva.
|
||||
CREATE OR REPLACE FUNCTION public.fc_cambiar_propia_contrasena(
|
||||
p_token UUID,
|
||||
p_password_actual TEXT,
|
||||
p_password_nueva TEXT
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO extensions, public
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_user_id UUID;
|
||||
v_stored_password_hash TEXT;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'cambiar_propia_contrasena');
|
||||
|
||||
IF p_password_nueva IS NULL OR length(p_password_nueva) < 8 THEN
|
||||
RAISE EXCEPTION 'La contraseña nueva debe tener al menos 8 caracteres.';
|
||||
END IF;
|
||||
|
||||
SELECT usuario_id INTO v_user_id FROM internal.sesiones WHERE token = p_token;
|
||||
|
||||
SELECT password_hash INTO v_stored_password_hash
|
||||
FROM public.usuarios
|
||||
WHERE id = v_user_id
|
||||
FOR UPDATE;
|
||||
|
||||
IF v_stored_password_hash IS NULL
|
||||
OR v_stored_password_hash <> crypt(p_password_actual, v_stored_password_hash) THEN
|
||||
RAISE EXCEPTION 'La contraseña actual es incorrecta.';
|
||||
END IF;
|
||||
|
||||
UPDATE public.usuarios
|
||||
SET password_hash = internal.create_password_hash(p_password_nueva)
|
||||
WHERE id = v_user_id;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
-- 6. fc_resetear_contrasena_usuario: reset administrativo (solo superadmin,
|
||||
-- sobre cualquier no-cliente, sin pedir la actual del objetivo). Función
|
||||
-- nueva.
|
||||
CREATE OR REPLACE FUNCTION public.fc_resetear_contrasena_usuario(
|
||||
p_token UUID,
|
||||
p_usuario_id UUID,
|
||||
p_password_nueva TEXT
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path TO extensions, public
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_rol_objetivo TEXT;
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token := p_token, p_accion := 'resetear_contrasena_usuario');
|
||||
|
||||
IF p_password_nueva IS NULL OR length(p_password_nueva) < 8 THEN
|
||||
RAISE EXCEPTION 'La contraseña nueva debe tener al menos 8 caracteres.';
|
||||
END IF;
|
||||
|
||||
SELECT rol INTO v_rol_objetivo
|
||||
FROM public.usuarios
|
||||
WHERE id = p_usuario_id
|
||||
FOR UPDATE;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'Usuario no encontrado.';
|
||||
END IF;
|
||||
|
||||
IF v_rol_objetivo = 'cliente' THEN
|
||||
RAISE EXCEPTION 'No se puede resetear la contraseña de un cliente: no inician sesión en este panel.';
|
||||
END IF;
|
||||
|
||||
UPDATE public.usuarios
|
||||
SET password_hash = internal.create_password_hash(p_password_nueva)
|
||||
WHERE id = p_usuario_id;
|
||||
END;
|
||||
$function$;
|
||||
+1931
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user