diff --git a/.env b/.env deleted file mode 100644 index b8af0aa..0000000 --- a/.env +++ /dev/null @@ -1,35 +0,0 @@ -# Archivo de variables de entorno para la configuración de Google Sheets -GOOGLE_SERVICE_ACCOUNT_EMAIL=dbsheets@dbsheets-487314.iam.gserviceaccount.com -GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCi2DVTbgH5WzE4\nrsT4BLcjplwszwkiZmhUxcVma/xI/6G+LQpzWTmmETmPWGFBpPukcoEDJDIugSFF\nuiKM+cSJIJJTYYWb5sBGXC6bi36AiA3W/zuRYGoFdNP+34iqepWpXpjKY8T0bbJJ\njhIgjW/SN2fYZeVBZqLATEzRqCcPYXI5gs9fyb2bzTEjiB3kCklVCudl/JDoxAWm\nJ9vO/OOXEgt/t97v9cMoo9OZ4Y3txqiQB41UdS97btDvpezc59u+8K9s5FT+yljC\nMyfRiRM+4MpycCoJobiT8NnCaNblMTd8mkfuujrFvnT/k9O794gBOJ2sgF7AYej4\nzm9tXpknAgMBAAECggEAM50bqcngTkydGT4rqAC2m1ILPRiR6JlU0CmvG3t2hxyf\nA0v7V/wbzYsAF3MHwGquZ7zzLy+1yA+doCAS+2Pe6yeruUnbs0I8BxEIpLxd6Bc3\na8GR+833TKtu1gW7p20bKoeVHfDpaB+stquVC0RJAPQWsfv54fTJ+PE2F+2YE8Np\njhWvM4OkUNZVBPZxG+RF0gzylIx2tw3QpLN3DMSr9Z2wGfohp7yryP96qqC1RFGQ\nCsaHsPRfzv+UNinGmA0Yzv79Stuqgsg+J3V/01Pm4ANutHix/kP0YAYCyg29UtVZ\nIJZtPOAfHWICYw+hQMpcfSLWzbkUQpwTTG0Bk6xmwQKBgQDUNf226UzdAm/V2Pag\nc1YF4P6Bru1A5jiFTpyrAYPSN+Wk84Gkl8XtuDDb7zXL+MaVb8aIVXTKhM2b0RDw\nHtDayoCCXci8VfP4sDRzjEGB1O1OPOl/wPD6L94CmNB0QeepNs4Q5xD0HEsoNN6s\nZGafIkIbVVUBCkuZwNxoLVXSKQKBgQDEcnKkgE/ror9vXSj0XY9Rn0wU2W6DreUt\nMizWyH9R0wBbaSJQ/4Wd3F6co2H2DSuZgXxLJvsPlqffUivs/oZnlx8HhWi6vjLL\nvSiqNC1D+Bh5TT7PEonENU99lruKtZfKcMpRtVF6WycW5ZlYok4lRCEBBvbjtGgr\nIHnqc8WazwKBgD0oHilDjQI4fJBtiY3uD0F8ePxYtEk6z9kjMaVat2my8DycbKVJ\nlCIRLguEIcXZuSlB4D5UGmdu5G5eTvpUdy5Go8huTLZyMvBdn5AZQJuxCH6+sTA/\nI3OGrf2jmpeWkrPCdpqKbOlc6g/5RJ78BFMiJVkJO4kkNWtnGF3xisUZAoGAJ8VT\n2GHxzC7CuvUNWYunfErXyFDMwvPttdy2nzUwMYyaQedi/yr8Dh2TOfsJ1hqMfSNt\n2nkl0t4ZVmj4Y56T71z09zzXGxduiTjehrbRgzUzHzu4P4vtQD/au/5MMaTZ+i4j\nh69Bs5fIriYFiaAyWfEVDXQvf6IChNiqVgDiPJkCgYEAooIiW4HHVUNGMnIeB7UI\nBXv5gSs2heHYSmEK+Er7In5cfJQzkcJvwiOMfOPCNuwVPn+ow5IN8le1acDon5I2\n0OsXhWmgyg9zmgguj273mH3h6+o6XTg5GrEYgPVpU3m2dJkd05oWhQvzy7/OxHs8\nE4ZMz97uqmU2r8XJCo//tv8=\n-----END PRIVATE KEY-----\n" -GOOGLE_SHEET_ID=12X6qeU0W4ZDw00vS4OreyC4nFjKBeBYo7rq3wyRsrNQ - -# Archivo de variables de entorno para la configuración de envío de emails -EMAIL_API_KEY=9UShpS8oh5Iun92TJSfevElI3Lp99TCv - -# This was inserted by prisma init: -# Environment variables declared in this file are NOT automatically loaded by Prisma. -# Please add import "dotenv/config"; to your prisma.config.ts file, or use the Prisma CLI with Bun -# to load environment variables from .env files: https://pris.ly/prisma-config-env-vars. - -# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. -# See the documentation for all the connection string options: https://pris.ly/d/connection-strings - -# The following prisma+postgres URL is similar to the URL produced by running a local Prisma Postgres -# server with the prisma dev CLI command, when not choosing any non-default ports or settings. The API key, unlike the -# one found in a remote Prisma Postgres URL, does not contain any sensitive information. - -DATABASE_URL="file:./dev.db" - -N8N_WEBHOOK_URL="https://flows2.carpa.com/webhook/news-summary" -N8N_API_KEY="sk_live_JwjpTdZrLVvijCKuWylWZbUuhBm6zPDH" - -# Cloudflare Email Sending -CLOUDFLARE_API_TOKEN=cfut_8Y0Tcwiv3v0K1LDK9QSvc3BjlU1lZFs1zikACgTw33977b37 -CLOUDFLARE_ACCOUNT_ID=3e689750123db91e81d3542d2930a907 - -# Cloudflare Turnstile — public key (visible in frontend) -TURNSTILE_SITE_KEY=0x4AAAAAAD9IYWC6HPflpneO - -# Cloudflare Turnstile — secret key (server-side, keep safe!) -# In production, set this as a Gitea Actions secret named TURNSTILE_SECRET_KEY -TURNSTILE_SECRET_KEY=0x4AAAAAAD9IYXASjQSpmo2iS902SDoXnT4 diff --git a/.env.example b/.env.example index 1162174..c366ac7 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,12 @@ SUPABASE_URL="http://localhost:8000" SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE" SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q" POSTGRES_PASSWORD=postgres + +# Admin auth — API key (para consumo externo de /api/admin/*) +ADMIN_API_KEY="CHANGE_ME_admin_api_key" + +# Admin auth — credenciales del seed (solo se usan en scripts/seed-admin.ts) +ADMIN_EMAIL="admin@example.com" +ADMIN_USERNAME="admin" +ADMIN_PASSWORD="CHANGE_ME_password" +ADMIN_NOMBRE="Admin" diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index c710ad6..264d40f 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -37,7 +37,7 @@ jobs: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} - source: "dist/*,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,ecosystem.config.cjs" + source: "dist/*,package.json,pnpm-lock.yaml,pnpm-workspace.yaml,ecosystem.config.cjs,prisma/schema.prisma,prisma/migrations,prisma.config.ts" target: "${{ env.TARGET_DIR }}" - name: Restart PM2 @@ -52,10 +52,21 @@ jobs: [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" npm install -g pnpm@11 2>&1 cd ${{ env.TARGET_DIR }} + cat > .env <<'ENVEOF' + DATABASE_URL="${{ secrets.DATABASE_URL }}" + DIRECT_URL="${{ secrets.DIRECT_URL }}" + GOOGLE_SERVICE_ACCOUNT_EMAIL="${{ secrets.GOOGLE_SERVICE_ACCOUNT_EMAIL }}" + GOOGLE_PRIVATE_KEY="${{ secrets.GOOGLE_PRIVATE_KEY }}" + GOOGLE_SHEET_ID="${{ secrets.GOOGLE_SHEET_ID }}" + EMAIL_API_KEY="${{ secrets.EMAIL_API_KEY }}" + TURNSTILE_SITE_KEY="${{ secrets.TURNSTILE_SITE_KEY }}" + TURNSTILE_SECRET_KEY="${{ secrets.TURNSTILE_SECRET_KEY }}" + CLOUDFLARE_API_TOKEN="${{ secrets.CLOUDFLARE_API_TOKEN }}" + CLOUDFLARE_ACCOUNT_ID="${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" + ADMIN_API_KEY="${{ secrets.ADMIN_API_KEY }}" + ENVEOF pnpm install --prod 2>&1 - sed -i "s|TURNSTILE_SITE_KEY: \"\"|TURNSTILE_SITE_KEY: \"${{ secrets.TURNSTILE_SITE_KEY }}\"|g" ecosystem.config.cjs - sed -i "s|TURNSTILE_SECRET_KEY: \"\"|TURNSTILE_SECRET_KEY: \"${{ secrets.TURNSTILE_SECRET_KEY }}\"|g" ecosystem.config.cjs - sed -i "s|CLOUDFLARE_API_TOKEN: \"\"|CLOUDFLARE_API_TOKEN: \"${{ secrets.CLOUDFLARE_API_TOKEN }}\"|g" ecosystem.config.cjs - sed -i "s|CLOUDFLARE_ACCOUNT_ID: \"\"|CLOUDFLARE_ACCOUNT_ID: \"${{ secrets.CLOUDFLARE_ACCOUNT_ID }}\"|g" ecosystem.config.cjs + pnpm exec prisma generate 2>&1 + pnpm exec prisma migrate deploy 2>&1 pm2 reload ecosystem.config.cjs --only ${{ env.APP_NAME }} --update-env || \ pm2 start ecosystem.config.cjs --only ${{ env.APP_NAME }} --update-env diff --git a/.gitignore b/.gitignore index 8ad93c8..e6be9db 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ pnpm-debug.log* # environment variables +.env .env.keys # macOS-specific files @@ -31,5 +32,12 @@ data/ opencode/ .opencode/ docs/ + +# guía técnica local +docs/GUIA-TECNICA.md + +# scripts de migración (locales, no en producción) +scripts/migrate-sheets-to-db.ts +scripts/seed-form-config.ts scripts/ .env.example diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs index 51ab06c..d03dcdb 100644 --- a/ecosystem.config.cjs +++ b/ecosystem.config.cjs @@ -2,26 +2,22 @@ module.exports = { apps: [ { name: "cdrdpyj", + cwd: __dirname, script: "dist/server/entry.mjs", + interpreter_args: "--env-file=.env", env: { NODE_ENV: "staging", PORT: 3310, - TURNSTILE_SITE_KEY: "", - TURNSTILE_SECRET_KEY: "", - CLOUDFLARE_API_TOKEN: "", - CLOUDFLARE_ACCOUNT_ID: "", } }, { name: "cdrdpyj-live", + cwd: __dirname, script: "dist/server/entry.mjs", + interpreter_args: "--env-file=.env", env: { NODE_ENV: "production", PORT: 4321, - TURNSTILE_SITE_KEY: "", - TURNSTILE_SECRET_KEY: "", - CLOUDFLARE_API_TOKEN: "", - CLOUDFLARE_ACCOUNT_ID: "", } } ] diff --git a/package.json b/package.json index 9d34b4c..8e56ca6 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,10 @@ "build": "astro build", "postbuild": "node scripts/send-to-n8n.js", "preview": "astro preview", - "astro": "astro" + "astro": "astro", + "db:migrate-sheets": "npx tsx scripts/migrate-sheets-to-db.ts", + "db:seed-form-config": "npx tsx scripts/seed-form-config.ts", + "db:setup": "npx tsx scripts/seed-form-config.ts && npx tsx scripts/migrate-sheets-to-db.ts" }, "dependencies": { "@astrojs/markdoc": "^2.0.3", @@ -42,6 +45,7 @@ }, "devDependencies": { "@tailwindcss/typography": "^0.5.19", - "daisyui": "^5.5.18" + "daisyui": "^5.5.18", + "dotenv": "^17.4.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4932aba..1ed8852 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,9 @@ importers: daisyui: specifier: ^5.5.18 version: 5.5.20 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 packages: diff --git a/prisma.config.ts b/prisma.config.ts index 8ded1a5..8705955 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -11,6 +11,6 @@ export default defineConfig({ }, engine: "classic", datasource: { - url: env("DATABASE_URL"), + url: env("DIRECT_URL") || env("DATABASE_URL"), }, }); diff --git a/prisma/migrations/20260220004001_init/migration.sql b/prisma/migrations-sqlite-backup/20260220004001_init/migration.sql similarity index 100% rename from prisma/migrations/20260220004001_init/migration.sql rename to prisma/migrations-sqlite-backup/20260220004001_init/migration.sql diff --git a/prisma/migrations/20260220004224_add_contact/migration.sql b/prisma/migrations-sqlite-backup/20260220004224_add_contact/migration.sql similarity index 100% rename from prisma/migrations/20260220004224_add_contact/migration.sql rename to prisma/migrations-sqlite-backup/20260220004224_add_contact/migration.sql diff --git a/prisma/migrations/20260220005204_make_email_unique/migration.sql b/prisma/migrations-sqlite-backup/20260220005204_make_email_unique/migration.sql similarity index 100% rename from prisma/migrations/20260220005204_make_email_unique/migration.sql rename to prisma/migrations-sqlite-backup/20260220005204_make_email_unique/migration.sql diff --git a/prisma/migrations-sqlite-backup/migration_lock.toml b/prisma/migrations-sqlite-backup/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations-sqlite-backup/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/migrations/20260803025550_init/migration.sql b/prisma/migrations/20260803025550_init/migration.sql new file mode 100644 index 0000000..b0fd2e1 --- /dev/null +++ b/prisma/migrations/20260803025550_init/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Contact" ( + "id" SERIAL NOT NULL, + "nombre" TEXT NOT NULL, + "email" TEXT NOT NULL, + "mensaje" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Contact_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Contact_email_key" ON "Contact"("email"); + +-- CreateIndex +CREATE INDEX "Contact_email_createdAt_idx" ON "Contact"("email", "createdAt"); diff --git a/prisma/migrations/20260803032753_add_form_models/migration.sql b/prisma/migrations/20260803032753_add_form_models/migration.sql new file mode 100644 index 0000000..4bef4e8 --- /dev/null +++ b/prisma/migrations/20260803032753_add_form_models/migration.sql @@ -0,0 +1,174 @@ +/* + Warnings: + + - You are about to drop the `Contact` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "Contact"; + +-- CreateTable +CREATE TABLE "contact" ( + "id" SERIAL NOT NULL, + "nombre" TEXT NOT NULL, + "email" TEXT NOT NULL, + "mensaje" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "contact_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "formularios" ( + "id" UUID NOT NULL, + "numero_voluntario" SERIAL NOT NULL, + "nombre" TEXT NOT NULL, + "segundo_nombre" TEXT, + "apellido" TEXT NOT NULL, + "documento_nro" TEXT, + "documento_tipo" TEXT, + "correo" TEXT NOT NULL, + "fecha_nacimiento" DATE, + "nacionalidad" TEXT, + "sexo" TEXT, + "direccion" TEXT, + "ciudad" TEXT, + "estado" TEXT, + "pais" TEXT, + "codigo_postal" TEXT, + "telefono" TEXT, + "whatsapp" TEXT, + "profesion" TEXT, + "lugar_trabajo" TEXT, + "nivel_academico" TEXT, + "status" TEXT NOT NULL DEFAULT 'pendiente', + "form_version" TEXT, + "email_sent_at" TIMESTAMPTZ, + "submitted_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "formularios_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "respuestas" ( + "id" UUID NOT NULL, + "formulario_id" UUID NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "seccion" TEXT NOT NULL DEFAULT '', + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "respuestas_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "secciones" ( + "id" UUID NOT NULL, + "title_key" TEXT NOT NULL, + "title" TEXT, + "orden" INTEGER NOT NULL DEFAULT 0, + "columns" INTEGER NOT NULL DEFAULT 1, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "secciones_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "campos" ( + "id" UUID NOT NULL, + "key" TEXT NOT NULL, + "label_key" TEXT NOT NULL, + "tipo" TEXT NOT NULL, + "seccion_id" UUID, + "required" BOOLEAN NOT NULL DEFAULT false, + "orden" INTEGER NOT NULL DEFAULT 0, + "options" JSONB, + "source" TEXT, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "campos_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "form_versions" ( + "id" UUID NOT NULL, + "version" TEXT NOT NULL, + "config" JSONB NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "form_versions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "admins" ( + "id" UUID NOT NULL, + "auth_user_id" UUID NOT NULL, + "email" TEXT NOT NULL, + "nombre" TEXT NOT NULL, + "rol" TEXT NOT NULL DEFAULT 'coordinador', + "activo" BOOLEAN NOT NULL DEFAULT true, + "ultimo_acceso" TIMESTAMPTZ, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "admins_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "contact_email_key" ON "contact"("email"); + +-- CreateIndex +CREATE INDEX "contact_email_createdAt_idx" ON "contact"("email", "createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "formularios_numero_voluntario_key" ON "formularios"("numero_voluntario"); + +-- CreateIndex +CREATE INDEX "formularios_correo_idx" ON "formularios"("correo"); + +-- CreateIndex +CREATE INDEX "formularios_status_idx" ON "formularios"("status"); + +-- CreateIndex +CREATE INDEX "formularios_pais_idx" ON "formularios"("pais"); + +-- CreateIndex +CREATE INDEX "formularios_submitted_at_idx" ON "formularios"("submitted_at"); + +-- CreateIndex +CREATE INDEX "respuestas_formulario_id_idx" ON "respuestas"("formulario_id"); + +-- CreateIndex +CREATE INDEX "respuestas_key_idx" ON "respuestas"("key"); + +-- CreateIndex +CREATE INDEX "respuestas_seccion_idx" ON "respuestas"("seccion"); + +-- CreateIndex +CREATE INDEX "secciones_orden_idx" ON "secciones"("orden"); + +-- CreateIndex +CREATE UNIQUE INDEX "campos_key_key" ON "campos"("key"); + +-- CreateIndex +CREATE INDEX "campos_seccion_id_idx" ON "campos"("seccion_id"); + +-- CreateIndex +CREATE INDEX "campos_tipo_idx" ON "campos"("tipo"); + +-- CreateIndex +CREATE UNIQUE INDEX "admins_auth_user_id_key" ON "admins"("auth_user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "admins_email_key" ON "admins"("email"); + +-- CreateIndex +CREATE INDEX "admins_email_idx" ON "admins"("email"); + +-- AddForeignKey +ALTER TABLE "respuestas" ADD CONSTRAINT "respuestas_formulario_id_fkey" FOREIGN KEY ("formulario_id") REFERENCES "formularios"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "campos" ADD CONSTRAINT "campos_seccion_id_fkey" FOREIGN KEY ("seccion_id") REFERENCES "secciones"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260813010740_add_trigram_search_indexes/migration.sql b/prisma/migrations/20260813010740_add_trigram_search_indexes/migration.sql new file mode 100644 index 0000000..7ca2f3a --- /dev/null +++ b/prisma/migrations/20260813010740_add_trigram_search_indexes/migration.sql @@ -0,0 +1,23 @@ +-- Enable pg_trgm for trigram-based GIN indexes +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- CreateIndex +CREATE INDEX "formularios_nombre_trgm_idx" ON "formularios" USING GIN ("nombre" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_apellido_trgm_idx" ON "formularios" USING GIN ("apellido" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_correo_trgm_idx" ON "formularios" USING GIN ("correo" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_telefono_trgm_idx" ON "formularios" USING GIN ("telefono" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_whatsapp_trgm_idx" ON "formularios" USING GIN ("whatsapp" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_documento_nro_trgm_idx" ON "formularios" USING GIN ("documento_nro" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "respuestas_value_trgm_idx" ON "respuestas" USING GIN ("value" gin_trgm_ops); diff --git a/prisma/migrations/20260815180714_add_trigram_fuzzy_indexes/migration.sql b/prisma/migrations/20260815180714_add_trigram_fuzzy_indexes/migration.sql new file mode 100644 index 0000000..9295739 --- /dev/null +++ b/prisma/migrations/20260815180714_add_trigram_fuzzy_indexes/migration.sql @@ -0,0 +1,14 @@ +-- CreateIndex +CREATE INDEX "formularios_ciudad_trgm_idx" ON "formularios" USING GIN ("ciudad" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_estado_trgm_idx" ON "formularios" USING GIN ("estado" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_profesion_trgm_idx" ON "formularios" USING GIN ("profesion" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_nivel_academico_trgm_idx" ON "formularios" USING GIN ("nivel_academico" gin_trgm_ops); + +-- CreateIndex +CREATE INDEX "formularios_nacionalidad_trgm_idx" ON "formularios" USING GIN ("nacionalidad" gin_trgm_ops); diff --git a/prisma/migrations/20260816105155_add_admin_auth/migration.sql b/prisma/migrations/20260816105155_add_admin_auth/migration.sql new file mode 100644 index 0000000..3cfa438 --- /dev/null +++ b/prisma/migrations/20260816105155_add_admin_auth/migration.sql @@ -0,0 +1,25 @@ +-- AlterTable +ALTER TABLE "admins" ALTER COLUMN "auth_user_id" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "admins" ADD COLUMN "password_hash" TEXT; + +-- CreateTable +CREATE TABLE "sessions" ( + "id" UUID NOT NULL, + "token_hash" TEXT NOT NULL, + "admin_id" UUID NOT NULL, + "expires_at" TIMESTAMPTZ NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "sessions_token_hash_key" ON "sessions"("token_hash"); + +-- CreateIndex +CREATE INDEX "sessions_admin_id_idx" ON "sessions"("admin_id"); + +-- AddForeignKey +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_admin_id_fkey" FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260816122101_add_admin_username/migration.sql b/prisma/migrations/20260816122101_add_admin_username/migration.sql new file mode 100644 index 0000000..6fd773d --- /dev/null +++ b/prisma/migrations/20260816122101_add_admin_username/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "admins" ADD COLUMN "username" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "admins_username_key" ON "admins"("username"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index 2a5a444..044d57c 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (e.g., Git) -provider = "sqlite" +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2aa9ec2..95a8873 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,8 +3,9 @@ generator client { } datasource db { - provider = "sqlite" - url = env("DATABASE_URL") + provider = "postgresql" + url = env("DATABASE_URL") + directUrl = env("DIRECT_URL") } model Contact { @@ -16,4 +17,144 @@ model Contact { updatedAt DateTime @updatedAt @@index([email, createdAt]) -} \ No newline at end of file + @@map("contact") +} + +model Formulario { + id String @id @default(uuid()) @db.Uuid + numero_voluntario Int @unique @default(autoincrement()) + nombre String + segundo_nombre String? + apellido String + documento_nro String? + documento_tipo String? + correo String + fecha_nacimiento DateTime? @db.Date + nacionalidad String? + sexo String? + direccion String? + ciudad String? + estado String? + pais String? + codigo_postal String? + telefono String? + whatsapp String? + profesion String? + lugar_trabajo String? + nivel_academico String? + status String @default("pendiente") + form_version String? + email_sent_at DateTime? @db.Timestamptz + submitted_at DateTime @default(now()) @db.Timestamptz + updated_at DateTime @updatedAt @db.Timestamptz + + respuestas Respuesta[] + + @@index([correo]) + @@index([status]) + @@index([pais]) + @@index([submitted_at]) + @@index([nombre(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_nombre_trgm_idx") + @@index([apellido(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_apellido_trgm_idx") + @@index([correo(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_correo_trgm_idx") + @@index([telefono(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_telefono_trgm_idx") + @@index([whatsapp(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_whatsapp_trgm_idx") + @@index([documento_nro(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_documento_nro_trgm_idx") + @@index([ciudad(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_ciudad_trgm_idx") + @@index([estado(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_estado_trgm_idx") + @@index([profesion(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_profesion_trgm_idx") + @@index([nivel_academico(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_nivel_academico_trgm_idx") + @@index([nacionalidad(ops: raw("gin_trgm_ops"))], type: Gin, map: "formularios_nacionalidad_trgm_idx") + @@map("formularios") +} + +model Respuesta { + id String @id @default(uuid()) @db.Uuid + formulario_id String @db.Uuid + key String + value String + seccion String @default("") + created_at DateTime @default(now()) @db.Timestamptz + + formulario Formulario @relation(fields: [formulario_id], references: [id], onDelete: Cascade) + + @@index([formulario_id]) + @@index([key]) + @@index([seccion]) + @@index([value(ops: raw("gin_trgm_ops"))], type: Gin, map: "respuestas_value_trgm_idx") + @@map("respuestas") +} + +model Seccion { + id String @id @default(uuid()) @db.Uuid + title_key String + title String? + orden Int @default(0) + columns Int @default(1) + created_at DateTime @default(now()) @db.Timestamptz + + campos Campo[] + + @@index([orden]) + @@map("secciones") +} + +model Campo { + id String @id @default(uuid()) @db.Uuid + key String @unique + label_key String + tipo String + seccion_id String? @db.Uuid + required Boolean @default(false) + orden Int @default(0) + options Json? @db.JsonB + source String? + created_at DateTime @default(now()) @db.Timestamptz + + seccion Seccion? @relation(fields: [seccion_id], references: [id], onDelete: SetNull) + + @@index([seccion_id]) + @@index([tipo]) + @@map("campos") +} + +model FormVersion { + id String @id @default(uuid()) @db.Uuid + version String + config Json @db.JsonB + active Boolean @default(false) + created_at DateTime @default(now()) @db.Timestamptz + + @@map("form_versions") +} + +model Admin { + id String @id @default(uuid()) @db.Uuid + auth_user_id String? @unique @db.Uuid + email String @unique + username String? @unique + nombre String + password_hash String? + rol String @default("coordinador") + activo Boolean @default(true) + ultimo_acceso DateTime? @db.Timestamptz + created_at DateTime @default(now()) @db.Timestamptz + + sessions Session[] + + @@index([email]) + @@map("admins") +} + +model Session { + id String @id @default(uuid()) @db.Uuid + token_hash String @unique + admin_id String @db.Uuid + expires_at DateTime @db.Timestamptz + created_at DateTime @default(now()) @db.Timestamptz + + admin Admin @relation(fields: [admin_id], references: [id], onDelete: Cascade) + + @@index([admin_id]) + @@map("sessions") +} diff --git a/public/admin/index.html b/public/admin/index.html deleted file mode 100644 index 13d19a5..0000000 --- a/public/admin/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - TinaCMS - - - - - - - - -
- - \ No newline at end of file diff --git a/src/components/Header.astro b/src/components/Header.astro index f210430..da02ec4 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -12,6 +12,7 @@ const tl = createTranslator(Astro.currentLocale); const currentLocale = Astro.currentLocale; const currentPath = Astro.url.pathname; const { locale } = Astro.params; +const isAdmin = currentPath.split("/").includes("admin"); const languages = [ { code: "es", icon: "icon_flag_es", label: "Español" }, @@ -156,6 +157,18 @@ function translatePath(newLocale: string) { )) } + {isAdmin && ( +
  • +
    + +
    +
  • + )}
    + {isAdmin && ( + + )}