Files
Spanglish/backend/src/db/migrate.ts
root 47ba754f05 Add full SEO optimization for Spanglish social and language events
- Add comprehensive metadata to root layout with Open Graph, Twitter cards
- Create dynamic sitemap.ts for all pages and events
- Create robots.ts with proper allow/disallow rules
- Add JSON-LD Event structured data to event detail pages
- Add page-specific metadata to events, community, contact, FAQ pages
- Add FAQ structured data schema
- Update footer with local SEO text for Asunción, Paraguay
- Add web manifest for mobile SEO
- Create 404 page with proper noindex
- Optimize image alt text and add lazy loading
- Add NEXT_PUBLIC_SITE_URL env variable
- Add about/ folder to gitignore
2026-01-30 21:05:25 +00:00

635 lines
20 KiB
TypeScript

import { db } from './index.js';
import { sql } from 'drizzle-orm';
const dbType = process.env.DB_TYPE || 'sqlite';
async function migrate() {
console.log('Running migrations...');
if (dbType === 'sqlite') {
// SQLite migrations
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password TEXT,
name TEXT NOT NULL,
phone TEXT,
role TEXT NOT NULL DEFAULT 'user',
language_preference TEXT,
is_claimed INTEGER NOT NULL DEFAULT 1,
google_id TEXT,
ruc_number TEXT,
account_status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`);
// Add new user columns if they don't exist (for existing databases)
try {
await (db as any).run(sql`ALTER TABLE users ADD COLUMN is_claimed INTEGER NOT NULL DEFAULT 1`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE users ADD COLUMN google_id TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE users ADD COLUMN ruc_number TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE users ADD COLUMN account_status TEXT NOT NULL DEFAULT 'active'`);
} catch (e) { /* column may already exist */ }
// Magic link tokens table
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS magic_link_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
token TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
expires_at TEXT NOT NULL,
used_at TEXT,
created_at TEXT NOT NULL
)
`);
// User sessions table
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS user_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
token TEXT NOT NULL UNIQUE,
user_agent TEXT,
ip_address TEXT,
last_active_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL
)
`);
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
title_es TEXT,
description TEXT NOT NULL,
description_es TEXT,
start_datetime TEXT NOT NULL,
end_datetime TEXT,
location TEXT NOT NULL,
location_url TEXT,
price REAL NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'PYG',
capacity INTEGER NOT NULL DEFAULT 50,
status TEXT NOT NULL DEFAULT 'draft',
banner_url TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`);
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS tickets (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
event_id TEXT NOT NULL REFERENCES events(id),
attendee_name TEXT NOT NULL,
attendee_email TEXT NOT NULL,
attendee_phone TEXT NOT NULL,
preferred_language TEXT,
status TEXT NOT NULL DEFAULT 'pending',
checkin_at TEXT,
qr_code TEXT,
created_at TEXT NOT NULL
)
`);
// Add new columns if they don't exist (for existing databases)
try {
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_name TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_email TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_phone TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN preferred_language TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN admin_note TEXT`);
} catch (e) { /* column may already exist */ }
// Migration: Add first/last name columns
try {
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_first_name TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_last_name TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE tickets ADD COLUMN attendee_ruc TEXT`);
} catch (e) { /* column may already exist */ }
// Migration: Copy data from attendee_name to attendee_first_name if attendee_first_name is empty
try {
await (db as any).run(sql`
UPDATE tickets
SET attendee_first_name = attendee_name
WHERE attendee_first_name IS NULL AND attendee_name IS NOT NULL
`);
} catch (e) { /* migration may have already run */ }
// Make attendee_email and attendee_phone nullable (recreate table if needed or just allow nulls for new entries)
// SQLite doesn't support altering column constraints, so we'll just ensure new entries work
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS payments (
id TEXT PRIMARY KEY,
ticket_id TEXT NOT NULL REFERENCES tickets(id),
provider TEXT NOT NULL,
amount REAL NOT NULL,
currency TEXT NOT NULL DEFAULT 'PYG',
status TEXT NOT NULL DEFAULT 'pending',
reference TEXT,
paid_at TEXT,
paid_by_admin_id TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`);
// Add new columns if they don't exist (for existing databases)
try {
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN paid_at TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN paid_by_admin_id TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN user_marked_paid_at TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN admin_note TEXT`);
} catch (e) { /* column may already exist */ }
// Invoices table
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS invoices (
id TEXT PRIMARY KEY,
payment_id TEXT NOT NULL REFERENCES payments(id),
user_id TEXT NOT NULL REFERENCES users(id),
invoice_number TEXT NOT NULL UNIQUE,
ruc_number TEXT,
legal_name TEXT,
amount REAL NOT NULL,
currency TEXT NOT NULL DEFAULT 'PYG',
pdf_url TEXT,
status TEXT NOT NULL DEFAULT 'generated',
created_at TEXT NOT NULL
)
`);
// Payment options table
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS payment_options (
id TEXT PRIMARY KEY,
tpago_enabled INTEGER NOT NULL DEFAULT 0,
tpago_link TEXT,
tpago_instructions TEXT,
tpago_instructions_es TEXT,
bank_transfer_enabled INTEGER NOT NULL DEFAULT 0,
bank_name TEXT,
bank_account_holder TEXT,
bank_account_number TEXT,
bank_alias TEXT,
bank_phone TEXT,
bank_notes TEXT,
bank_notes_es TEXT,
lightning_enabled INTEGER NOT NULL DEFAULT 1,
cash_enabled INTEGER NOT NULL DEFAULT 1,
cash_instructions TEXT,
cash_instructions_es TEXT,
updated_at TEXT NOT NULL,
updated_by TEXT REFERENCES users(id)
)
`);
// Add allow_duplicate_bookings column to payment_options if it doesn't exist
try {
await (db as any).run(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`);
} catch (e) { /* column may already exist */ }
// Event payment overrides table
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS event_payment_overrides (
id TEXT PRIMARY KEY,
event_id TEXT NOT NULL REFERENCES events(id),
tpago_enabled INTEGER,
tpago_link TEXT,
tpago_instructions TEXT,
tpago_instructions_es TEXT,
bank_transfer_enabled INTEGER,
bank_name TEXT,
bank_account_holder TEXT,
bank_account_number TEXT,
bank_alias TEXT,
bank_phone TEXT,
bank_notes TEXT,
bank_notes_es TEXT,
lightning_enabled INTEGER,
cash_enabled INTEGER,
cash_instructions TEXT,
cash_instructions_es TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`);
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS contacts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
message TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'new',
created_at TEXT NOT NULL
)
`);
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS email_subscribers (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL
)
`);
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS media (
id TEXT PRIMARY KEY,
file_url TEXT NOT NULL,
type TEXT NOT NULL,
related_id TEXT,
related_type TEXT,
created_at TEXT NOT NULL
)
`);
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id),
action TEXT NOT NULL,
target TEXT,
target_id TEXT,
details TEXT,
timestamp TEXT NOT NULL
)
`);
// Email system tables
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS email_templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
slug TEXT NOT NULL UNIQUE,
subject TEXT NOT NULL,
subject_es TEXT,
body_html TEXT NOT NULL,
body_html_es TEXT,
body_text TEXT,
body_text_es TEXT,
description TEXT,
variables TEXT,
is_system INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`);
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS email_logs (
id TEXT PRIMARY KEY,
template_id TEXT REFERENCES email_templates(id),
event_id TEXT REFERENCES events(id),
recipient_email TEXT NOT NULL,
recipient_name TEXT,
subject TEXT NOT NULL,
body_html TEXT,
status TEXT NOT NULL DEFAULT 'pending',
error_message TEXT,
sent_at TEXT,
sent_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL
)
`);
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS email_settings (
id TEXT PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`);
} else {
// PostgreSQL migrations
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255),
name VARCHAR(255) NOT NULL,
phone VARCHAR(50),
role VARCHAR(20) NOT NULL DEFAULT 'user',
language_preference VARCHAR(10),
is_claimed INTEGER NOT NULL DEFAULT 1,
google_id VARCHAR(255),
ruc_number VARCHAR(15),
account_status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
)
`);
// Add new user columns for existing PostgreSQL databases
try {
await (db as any).execute(sql`ALTER TABLE users ADD COLUMN is_claimed INTEGER NOT NULL DEFAULT 1`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).execute(sql`ALTER TABLE users ADD COLUMN google_id VARCHAR(255)`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).execute(sql`ALTER TABLE users ADD COLUMN ruc_number VARCHAR(15)`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).execute(sql`ALTER TABLE users ADD COLUMN account_status VARCHAR(20) NOT NULL DEFAULT 'active'`);
} catch (e) { /* column may already exist */ }
// Magic link tokens table
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS magic_link_tokens (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
token VARCHAR(255) NOT NULL UNIQUE,
type VARCHAR(30) NOT NULL,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP,
created_at TIMESTAMP NOT NULL
)
`);
// User sessions table
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS user_sessions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
token VARCHAR(500) NOT NULL UNIQUE,
user_agent TEXT,
ip_address VARCHAR(45),
last_active_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS events (
id UUID PRIMARY KEY,
title VARCHAR(255) NOT NULL,
title_es VARCHAR(255),
description TEXT NOT NULL,
description_es TEXT,
start_datetime TIMESTAMP NOT NULL,
end_datetime TIMESTAMP,
location VARCHAR(500) NOT NULL,
location_url VARCHAR(500),
price DECIMAL(10, 2) NOT NULL DEFAULT 0,
currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
capacity INTEGER NOT NULL DEFAULT 50,
status VARCHAR(20) NOT NULL DEFAULT 'draft',
banner_url VARCHAR(500),
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS tickets (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
event_id UUID NOT NULL REFERENCES events(id),
attendee_first_name VARCHAR(255) NOT NULL,
attendee_last_name VARCHAR(255),
attendee_email VARCHAR(255),
attendee_phone VARCHAR(50),
attendee_ruc VARCHAR(15),
preferred_language VARCHAR(10),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
checkin_at TIMESTAMP,
qr_code VARCHAR(255),
admin_note TEXT,
created_at TIMESTAMP NOT NULL
)
`);
// Add attendee_ruc column if it doesn't exist
try {
await (db as any).execute(sql`ALTER TABLE tickets ADD COLUMN attendee_ruc VARCHAR(15)`);
} catch (e) { /* column may already exist */ }
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS payments (
id UUID PRIMARY KEY,
ticket_id UUID NOT NULL REFERENCES tickets(id),
provider VARCHAR(50) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
status VARCHAR(20) NOT NULL DEFAULT 'pending',
reference VARCHAR(255),
user_marked_paid_at TIMESTAMP,
paid_at TIMESTAMP,
paid_by_admin_id UUID,
admin_note TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
)
`);
// Invoices table
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS invoices (
id UUID PRIMARY KEY,
payment_id UUID NOT NULL REFERENCES payments(id),
user_id UUID NOT NULL REFERENCES users(id),
invoice_number VARCHAR(50) NOT NULL UNIQUE,
ruc_number VARCHAR(15),
legal_name VARCHAR(255),
amount DECIMAL(10, 2) NOT NULL,
currency VARCHAR(10) NOT NULL DEFAULT 'PYG',
pdf_url VARCHAR(500),
status VARCHAR(20) NOT NULL DEFAULT 'generated',
created_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS payment_options (
id UUID PRIMARY KEY,
tpago_enabled INTEGER NOT NULL DEFAULT 0,
tpago_link VARCHAR(500),
tpago_instructions TEXT,
tpago_instructions_es TEXT,
bank_transfer_enabled INTEGER NOT NULL DEFAULT 0,
bank_name VARCHAR(255),
bank_account_holder VARCHAR(255),
bank_account_number VARCHAR(100),
bank_alias VARCHAR(100),
bank_phone VARCHAR(50),
bank_notes TEXT,
bank_notes_es TEXT,
lightning_enabled INTEGER NOT NULL DEFAULT 1,
cash_enabled INTEGER NOT NULL DEFAULT 1,
cash_instructions TEXT,
cash_instructions_es TEXT,
updated_at TIMESTAMP NOT NULL,
updated_by UUID REFERENCES users(id)
)
`);
// Add allow_duplicate_bookings column to payment_options if it doesn't exist
try {
await (db as any).execute(sql`ALTER TABLE payment_options ADD COLUMN allow_duplicate_bookings INTEGER NOT NULL DEFAULT 0`);
} catch (e) { /* column may already exist */ }
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS event_payment_overrides (
id UUID PRIMARY KEY,
event_id UUID NOT NULL REFERENCES events(id),
tpago_enabled INTEGER,
tpago_link VARCHAR(500),
tpago_instructions TEXT,
tpago_instructions_es TEXT,
bank_transfer_enabled INTEGER,
bank_name VARCHAR(255),
bank_account_holder VARCHAR(255),
bank_account_number VARCHAR(100),
bank_alias VARCHAR(100),
bank_phone VARCHAR(50),
bank_notes TEXT,
bank_notes_es TEXT,
lightning_enabled INTEGER,
cash_enabled INTEGER,
cash_instructions TEXT,
cash_instructions_es TEXT,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS contacts (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'new',
created_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS email_subscribers (
id UUID PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255),
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS media (
id UUID PRIMARY KEY,
file_url VARCHAR(500) NOT NULL,
type VARCHAR(20) NOT NULL,
related_id UUID,
related_type VARCHAR(50),
created_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
action VARCHAR(100) NOT NULL,
target VARCHAR(100),
target_id UUID,
details TEXT,
timestamp TIMESTAMP NOT NULL
)
`);
// Email system tables
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS email_templates (
id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
slug VARCHAR(100) NOT NULL UNIQUE,
subject VARCHAR(500) NOT NULL,
subject_es VARCHAR(500),
body_html TEXT NOT NULL,
body_html_es TEXT,
body_text TEXT,
body_text_es TEXT,
description TEXT,
variables TEXT,
is_system INTEGER NOT NULL DEFAULT 0,
is_active INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS email_logs (
id UUID PRIMARY KEY,
template_id UUID REFERENCES email_templates(id),
event_id UUID REFERENCES events(id),
recipient_email VARCHAR(255) NOT NULL,
recipient_name VARCHAR(255),
subject VARCHAR(500) NOT NULL,
body_html TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
error_message TEXT,
sent_at TIMESTAMP,
sent_by UUID REFERENCES users(id),
created_at TIMESTAMP NOT NULL
)
`);
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS email_settings (
id UUID PRIMARY KEY,
key VARCHAR(100) NOT NULL UNIQUE,
value TEXT NOT NULL,
updated_at TIMESTAMP NOT NULL
)
`);
}
console.log('Migrations completed successfully!');
process.exit(0);
}
migrate().catch((err) => {
console.error('Migration failed:', err);
process.exit(1);
});