Add PostgreSQL support with SQLite/Postgres database compatibility layer

- Add dbGet/dbAll helper functions for database-agnostic queries
- Add toDbBool/convertBooleansForDb for boolean type conversion
- Add toDbDate/getNow for timestamp type handling
- Add generateId that returns UUID for Postgres, nanoid for SQLite
- Update all routes to use compatibility helpers
- Add normalizeEvent to return clean number types from Postgres decimal
- Add formatPrice utility for consistent price display
- Add legal pages admin interface with RichTextEditor
- Update carousel images
- Add drizzle migration files for PostgreSQL
This commit is contained in:
Michilis
2026-02-02 03:46:35 +00:00
parent 9410e83b89
commit bafd1425c4
61 changed files with 5015 additions and 881 deletions

View File

@@ -0,0 +1,270 @@
CREATE TABLE `audit_logs` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text,
`action` text NOT NULL,
`target` text,
`target_id` text,
`details` text,
`timestamp` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `contacts` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL,
`message` text NOT NULL,
`status` text DEFAULT 'new' NOT NULL,
`created_at` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `email_logs` (
`id` text PRIMARY KEY NOT NULL,
`template_id` text,
`event_id` text,
`recipient_email` text NOT NULL,
`recipient_name` text,
`subject` text NOT NULL,
`body_html` text,
`status` text DEFAULT 'pending' NOT NULL,
`error_message` text,
`sent_at` text,
`sent_by` text,
`created_at` text NOT NULL,
FOREIGN KEY (`template_id`) REFERENCES `email_templates`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`event_id`) REFERENCES `events`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`sent_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `email_settings` (
`id` text PRIMARY KEY NOT NULL,
`key` text NOT NULL,
`value` text NOT NULL,
`updated_at` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `email_subscribers` (
`id` text PRIMARY KEY NOT NULL,
`email` text NOT NULL,
`name` text,
`status` text DEFAULT 'active' NOT NULL,
`created_at` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `email_templates` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`slug` text NOT NULL,
`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 DEFAULT false NOT NULL,
`is_active` integer DEFAULT true NOT NULL,
`created_at` text NOT NULL,
`updated_at` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `event_payment_overrides` (
`id` text PRIMARY KEY NOT NULL,
`event_id` text NOT NULL,
`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,
FOREIGN KEY (`event_id`) REFERENCES `events`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `events` (
`id` text PRIMARY KEY NOT NULL,
`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 DEFAULT 0 NOT NULL,
`currency` text DEFAULT 'PYG' NOT NULL,
`capacity` integer DEFAULT 50 NOT NULL,
`status` text DEFAULT 'draft' NOT NULL,
`banner_url` text,
`external_booking_enabled` integer DEFAULT false NOT NULL,
`external_booking_url` text,
`created_at` text NOT NULL,
`updated_at` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `invoices` (
`id` text PRIMARY KEY NOT NULL,
`payment_id` text NOT NULL,
`user_id` text NOT NULL,
`invoice_number` text NOT NULL,
`ruc_number` text,
`legal_name` text,
`amount` real NOT NULL,
`currency` text DEFAULT 'PYG' NOT NULL,
`pdf_url` text,
`status` text DEFAULT 'generated' NOT NULL,
`created_at` text NOT NULL,
FOREIGN KEY (`payment_id`) REFERENCES `payments`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `magic_link_tokens` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`token` text NOT NULL,
`type` text NOT NULL,
`expires_at` text NOT NULL,
`used_at` text,
`created_at` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `media` (
`id` text PRIMARY KEY NOT NULL,
`file_url` text NOT NULL,
`type` text NOT NULL,
`related_id` text,
`related_type` text,
`created_at` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `payment_options` (
`id` text PRIMARY KEY NOT NULL,
`tpago_enabled` integer DEFAULT false NOT NULL,
`tpago_link` text,
`tpago_instructions` text,
`tpago_instructions_es` text,
`bank_transfer_enabled` integer DEFAULT false NOT NULL,
`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 DEFAULT true NOT NULL,
`cash_enabled` integer DEFAULT true NOT NULL,
`cash_instructions` text,
`cash_instructions_es` text,
`allow_duplicate_bookings` integer DEFAULT false NOT NULL,
`updated_at` text NOT NULL,
`updated_by` text,
FOREIGN KEY (`updated_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `payments` (
`id` text PRIMARY KEY NOT NULL,
`ticket_id` text NOT NULL,
`provider` text NOT NULL,
`amount` real NOT NULL,
`currency` text DEFAULT 'PYG' NOT NULL,
`status` text DEFAULT 'pending' NOT NULL,
`reference` text,
`user_marked_paid_at` text,
`paid_at` text,
`paid_by_admin_id` text,
`admin_note` text,
`created_at` text NOT NULL,
`updated_at` text NOT NULL,
FOREIGN KEY (`ticket_id`) REFERENCES `tickets`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `site_settings` (
`id` text PRIMARY KEY NOT NULL,
`timezone` text DEFAULT 'America/Asuncion' NOT NULL,
`site_name` text DEFAULT 'Spanglish' NOT NULL,
`site_description` text,
`site_description_es` text,
`contact_email` text,
`contact_phone` text,
`facebook_url` text,
`instagram_url` text,
`twitter_url` text,
`linkedin_url` text,
`maintenance_mode` integer DEFAULT false NOT NULL,
`maintenance_message` text,
`maintenance_message_es` text,
`updated_at` text NOT NULL,
`updated_by` text,
FOREIGN KEY (`updated_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `tickets` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`event_id` text NOT NULL,
`attendee_first_name` text NOT NULL,
`attendee_last_name` text,
`attendee_email` text,
`attendee_phone` text,
`attendee_ruc` text,
`preferred_language` text,
`status` text DEFAULT 'pending' NOT NULL,
`checkin_at` text,
`checked_in_by_admin_id` text,
`qr_code` text,
`admin_note` text,
`created_at` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`event_id`) REFERENCES `events`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`checked_in_by_admin_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `user_sessions` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`token` text NOT NULL,
`user_agent` text,
`ip_address` text,
`last_active_at` text NOT NULL,
`expires_at` text NOT NULL,
`created_at` text NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` text PRIMARY KEY NOT NULL,
`email` text NOT NULL,
`password` text,
`name` text NOT NULL,
`phone` text,
`role` text DEFAULT 'user' NOT NULL,
`language_preference` text,
`is_claimed` integer DEFAULT true NOT NULL,
`google_id` text,
`ruc_number` text,
`account_status` text DEFAULT 'active' NOT NULL,
`created_at` text NOT NULL,
`updated_at` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `email_settings_key_unique` ON `email_settings` (`key`);--> statement-breakpoint
CREATE UNIQUE INDEX `email_subscribers_email_unique` ON `email_subscribers` (`email`);--> statement-breakpoint
CREATE UNIQUE INDEX `email_templates_name_unique` ON `email_templates` (`name`);--> statement-breakpoint
CREATE UNIQUE INDEX `email_templates_slug_unique` ON `email_templates` (`slug`);--> statement-breakpoint
CREATE UNIQUE INDEX `invoices_invoice_number_unique` ON `invoices` (`invoice_number`);--> statement-breakpoint
CREATE UNIQUE INDEX `magic_link_tokens_token_unique` ON `magic_link_tokens` (`token`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_sessions_token_unique` ON `user_sessions` (`token`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1769975033554,
"tag": "0000_steady_wendell_vaughn",
"breakpoints": true
}
]
}

View File

@@ -1,3 +1,4 @@
import 'dotenv/config';
import { drizzle as drizzleSqlite } from 'drizzle-orm/better-sqlite3';
import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres';
import Database from 'better-sqlite3';
@@ -29,5 +30,51 @@ if (dbType === 'postgres') {
db = drizzleSqlite(sqlite, { schema });
}
export { db };
// ==================== Database Compatibility Helpers ====================
// These functions abstract the differences between SQLite and PostgreSQL Drizzle drivers:
// - SQLite uses .get() for single result, .all() for multiple
// - PostgreSQL returns arrays directly (no .get()/.all() methods)
/**
* Get a single result from a query (works with both SQLite and PostgreSQL)
* @param query - A Drizzle query builder (e.g., db.select().from(table).where(...))
* @returns The first result or null
*/
export async function dbGet<T>(query: any): Promise<T | null> {
if (dbType === 'postgres') {
const results = await query;
return results[0] || null;
}
// SQLite - use .get()
return query.get() || null;
}
/**
* Get all results from a query (works with both SQLite and PostgreSQL)
* @param query - A Drizzle query builder (e.g., db.select().from(table).where(...))
* @returns Array of results
*/
export async function dbAll<T>(query: any): Promise<T[]> {
if (dbType === 'postgres') {
return await query;
}
// SQLite - use .all()
return query.all();
}
/**
* Check if using PostgreSQL
*/
export function isPostgres(): boolean {
return dbType === 'postgres';
}
/**
* Check if using SQLite
*/
export function isSqlite(): boolean {
return dbType === 'sqlite';
}
export { db, dbType };
export * from './schema.js';

View File

@@ -1,7 +1,10 @@
import 'dotenv/config';
import { db } from './index.js';
import { sql } from 'drizzle-orm';
const dbType = process.env.DB_TYPE || 'sqlite';
console.log(`Database type: ${dbType}`);
console.log(`Database URL: ${process.env.DATABASE_URL?.substring(0, 30)}...`);
async function migrate() {
console.log('Running migrations...');
@@ -384,6 +387,23 @@ async function migrate() {
updated_by TEXT REFERENCES users(id)
)
`);
// Legal pages table for admin-editable legal content
await (db as any).run(sql`
CREATE TABLE IF NOT EXISTS legal_pages (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
title_es TEXT,
content_text TEXT NOT NULL,
content_text_es TEXT,
content_markdown TEXT NOT NULL,
content_markdown_es TEXT,
updated_at TEXT NOT NULL,
updated_by TEXT REFERENCES users(id),
created_at TEXT NOT NULL
)
`);
} else {
// PostgreSQL migrations
await (db as any).execute(sql`
@@ -716,6 +736,23 @@ async function migrate() {
updated_by UUID REFERENCES users(id)
)
`);
// Legal pages table for admin-editable legal content
await (db as any).execute(sql`
CREATE TABLE IF NOT EXISTS legal_pages (
id UUID PRIMARY KEY,
slug VARCHAR(100) NOT NULL UNIQUE,
title VARCHAR(255) NOT NULL,
title_es VARCHAR(255),
content_text TEXT NOT NULL,
content_text_es TEXT,
content_markdown TEXT NOT NULL,
content_markdown_es TEXT,
updated_at TIMESTAMP NOT NULL,
updated_by UUID REFERENCES users(id),
created_at TIMESTAMP NOT NULL
)
`);
}
console.log('Migrations completed successfully!');

View File

@@ -249,6 +249,21 @@ export const sqliteEmailSettings = sqliteTable('email_settings', {
updatedAt: text('updated_at').notNull(),
});
// Legal Pages table for admin-editable legal content
export const sqliteLegalPages = sqliteTable('legal_pages', {
id: text('id').primaryKey(),
slug: text('slug').notNull().unique(),
title: text('title').notNull(), // English title
titleEs: text('title_es'), // Spanish title
contentText: text('content_text').notNull(), // Plain text edited by admin (English)
contentTextEs: text('content_text_es'), // Plain text edited by admin (Spanish)
contentMarkdown: text('content_markdown').notNull(), // Generated markdown for public display (English)
contentMarkdownEs: text('content_markdown_es'), // Generated markdown for public display (Spanish)
updatedAt: text('updated_at').notNull(),
updatedBy: text('updated_by').references(() => sqliteUsers.id),
createdAt: text('created_at').notNull(),
});
// Site Settings table for global website configuration
export const sqliteSiteSettings = sqliteTable('site_settings', {
id: text('id').primaryKey(),
@@ -512,6 +527,21 @@ export const pgEmailSettings = pgTable('email_settings', {
updatedAt: timestamp('updated_at').notNull(),
});
// Legal Pages table for admin-editable legal content
export const pgLegalPages = pgTable('legal_pages', {
id: uuid('id').primaryKey(),
slug: varchar('slug', { length: 100 }).notNull().unique(),
title: varchar('title', { length: 255 }).notNull(), // English title
titleEs: varchar('title_es', { length: 255 }), // Spanish title
contentText: pgText('content_text').notNull(), // Plain text edited by admin (English)
contentTextEs: pgText('content_text_es'), // Plain text edited by admin (Spanish)
contentMarkdown: pgText('content_markdown').notNull(), // Generated markdown for public display (English)
contentMarkdownEs: pgText('content_markdown_es'), // Generated markdown for public display (Spanish)
updatedAt: timestamp('updated_at').notNull(),
updatedBy: uuid('updated_by').references(() => pgUsers.id),
createdAt: timestamp('created_at').notNull(),
});
// Site Settings table for global website configuration
export const pgSiteSettings = pgTable('site_settings', {
id: uuid('id').primaryKey(),
@@ -556,6 +586,7 @@ export const magicLinkTokens = dbType === 'postgres' ? pgMagicLinkTokens : sqlit
export const userSessions = dbType === 'postgres' ? pgUserSessions : sqliteUserSessions;
export const invoices = dbType === 'postgres' ? pgInvoices : sqliteInvoices;
export const siteSettings = dbType === 'postgres' ? pgSiteSettings : sqliteSiteSettings;
export const legalPages = dbType === 'postgres' ? pgLegalPages : sqliteLegalPages;
// Type exports
export type User = typeof sqliteUsers.$inferSelect;
@@ -584,3 +615,5 @@ export type Invoice = typeof sqliteInvoices.$inferSelect;
export type NewInvoice = typeof sqliteInvoices.$inferInsert;
export type SiteSettings = typeof sqliteSiteSettings.$inferSelect;
export type NewSiteSettings = typeof sqliteSiteSettings.$inferInsert;
export type LegalPage = typeof sqliteLegalPages.$inferSelect;
export type NewLegalPage = typeof sqliteLegalPages.$inferInsert;

View File

@@ -20,6 +20,7 @@ import emailsRoutes from './routes/emails.js';
import paymentOptionsRoutes from './routes/payment-options.js';
import dashboardRoutes from './routes/dashboard.js';
import siteSettingsRoutes from './routes/site-settings.js';
import legalPagesRoutes from './routes/legal-pages.js';
import emailService from './lib/email.js';
const app = new Hono();
@@ -1714,6 +1715,7 @@ app.route('/api/emails', emailsRoutes);
app.route('/api/payment-options', paymentOptionsRoutes);
app.route('/api/dashboard', dashboardRoutes);
app.route('/api/site-settings', siteSettingsRoutes);
app.route('/api/legal-pages', legalPagesRoutes);
// 404 handler
app.notFound((c) => {

View File

@@ -3,7 +3,7 @@ import * as argon2 from 'argon2';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { Context } from 'hono';
import { db, users, magicLinkTokens, userSessions } from '../db/index.js';
import { db, dbGet, dbAll, users, magicLinkTokens, userSessions } from '../db/index.js';
import { eq, and, gt } from 'drizzle-orm';
import { generateId, getNow } from './utils.js';
@@ -72,16 +72,17 @@ export async function verifyMagicLinkToken(
): Promise<{ valid: boolean; userId?: string; error?: string }> {
const now = getNow();
const tokenRecord = await (db as any)
.select()
.from(magicLinkTokens)
.where(
and(
eq((magicLinkTokens as any).token, token),
eq((magicLinkTokens as any).type, type)
const tokenRecord = await dbGet<any>(
(db as any)
.select()
.from(magicLinkTokens)
.where(
and(
eq((magicLinkTokens as any).token, token),
eq((magicLinkTokens as any).type, type)
)
)
)
.get();
);
if (!tokenRecord) {
return { valid: false, error: 'Invalid token' };
@@ -132,16 +133,17 @@ export async function createUserSession(
export async function getUserSessions(userId: string) {
const now = getNow();
return (db as any)
.select()
.from(userSessions)
.where(
and(
eq((userSessions as any).userId, userId),
gt((userSessions as any).expiresAt, now)
return dbAll(
(db as any)
.select()
.from(userSessions)
.where(
and(
eq((userSessions as any).userId, userId),
gt((userSessions as any).expiresAt, now)
)
)
)
.all();
);
}
// Invalidate a specific session
@@ -208,7 +210,7 @@ export async function verifyToken(token: string): Promise<JWTPayload | null> {
}
}
export async function getAuthUser(c: Context) {
export async function getAuthUser(c: Context): Promise<any | null> {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return null;
@@ -221,7 +223,9 @@ export async function getAuthUser(c: Context) {
return null;
}
const user = await (db as any).select().from(users).where(eq((users as any).id, payload.sub)).get();
const user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).id, payload.sub))
);
return user || null;
}
@@ -243,6 +247,8 @@ export function requireAuth(roles?: string[]) {
}
export async function isFirstUser(): Promise<boolean> {
const result = await (db as any).select().from(users).limit(1).all();
const result = await dbAll(
(db as any).select().from(users).limit(1)
);
return !result || result.length === 0;
}

View File

@@ -1,10 +1,9 @@
// Email service for Spanglish platform
// Supports multiple email providers: Resend, SMTP (Nodemailer)
import { db, emailTemplates, emailLogs, events, tickets, payments, users, paymentOptions, eventPaymentOverrides } from '../db/index.js';
import { db, dbGet, dbAll, emailTemplates, emailLogs, events, tickets, payments, users, paymentOptions, eventPaymentOverrides } from '../db/index.js';
import { eq, and } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import { getNow } from './utils.js';
import { getNow, generateId } from './utils.js';
import {
replaceTemplateVariables,
wrapInBaseTemplate,
@@ -362,11 +361,12 @@ export const emailService = {
* Get a template by slug
*/
async getTemplate(slug: string): Promise<any | null> {
const template = await (db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).slug, slug))
.get();
const template = await dbGet(
(db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).slug, slug))
);
return template || null;
},
@@ -385,7 +385,7 @@ export const emailService = {
console.log(`[Email] Creating template: ${template.name}`);
await (db as any).insert(emailTemplates).values({
id: nanoid(),
id: generateId(),
name: template.name,
slug: template.slug,
subject: template.subject,
@@ -470,7 +470,7 @@ export const emailService = {
const finalBodyText = bodyText ? replaceTemplateVariables(bodyText, allVariables) : undefined;
// Create log entry
const logId = nanoid();
const logId = generateId();
const now = getNow();
await (db as any).insert(emailLogs).values({
@@ -525,21 +525,23 @@ export const emailService = {
*/
async sendBookingConfirmation(ticketId: string): Promise<{ success: boolean; error?: string }> {
// Get ticket with event info
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
.get();
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
);
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
if (!event) {
return { success: false, error: 'Event not found' };
@@ -580,31 +582,34 @@ export const emailService = {
*/
async sendPaymentReceipt(paymentId: string): Promise<{ success: boolean; error?: string }> {
// Get payment with ticket and event info
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, paymentId))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, paymentId))
);
if (!payment) {
return { success: false, error: 'Payment not found' };
}
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
);
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
if (!event) {
return { success: false, error: 'Event not found' };
@@ -643,17 +648,19 @@ export const emailService = {
*/
async getPaymentConfig(eventId: string): Promise<Record<string, any>> {
// Get global options
const globalOptions = await (db as any)
.select()
.from(paymentOptions)
.get();
const globalOptions = await dbGet<any>(
(db as any)
.select()
.from(paymentOptions)
);
// Get event overrides
const overrides = await (db as any)
.select()
.from(eventPaymentOverrides)
.where(eq((eventPaymentOverrides as any).eventId, eventId))
.get();
const overrides = await dbGet<any>(
(db as any)
.select()
.from(eventPaymentOverrides)
.where(eq((eventPaymentOverrides as any).eventId, eventId))
);
// Defaults
const defaults = {
@@ -696,33 +703,36 @@ export const emailService = {
*/
async sendPaymentInstructions(ticketId: string): Promise<{ success: boolean; error?: string }> {
// Get ticket
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
.get();
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
);
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
// Get event
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
if (!event) {
return { success: false, error: 'Event not found' };
}
// Get payment
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
);
if (!payment) {
return { success: false, error: 'Payment not found' };
@@ -797,33 +807,36 @@ export const emailService = {
*/
async sendPaymentRejectionEmail(paymentId: string): Promise<{ success: boolean; error?: string }> {
// Get payment
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, paymentId))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, paymentId))
);
if (!payment) {
return { success: false, error: 'Payment not found' };
}
// Get ticket
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
);
if (!ticket) {
return { success: false, error: 'Ticket not found' };
}
// Get event
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
if (!event) {
return { success: false, error: 'Event not found' };
@@ -872,11 +885,12 @@ export const emailService = {
const { eventId, templateSlug, customVariables = {}, recipientFilter = 'confirmed', sentBy } = params;
// Get event
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, eventId))
);
if (!event) {
return { success: false, sentCount: 0, failedCount: 0, errors: ['Event not found'] };
@@ -897,7 +911,7 @@ export const emailService = {
);
}
const eventTickets = await ticketQuery.all();
const eventTickets = await dbAll<any>(ticketQuery);
if (eventTickets.length === 0) {
return { success: true, sentCount: 0, failedCount: 0, errors: ['No recipients found'] };
@@ -971,7 +985,7 @@ export const emailService = {
const finalBodyHtml = wrapInBaseTemplate(bodyHtml, allVariables);
// Create log entry
const logId = nanoid();
const logId = generateId();
const now = getNow();
await (db as any).insert(emailLogs).values({

View File

@@ -1,15 +1,71 @@
import { nanoid } from 'nanoid';
import { randomUUID } from 'crypto';
/**
* Get database type (reads env var each time to handle module loading order)
*/
function getDbType(): string {
return process.env.DB_TYPE || 'sqlite';
}
/**
* Generate a unique ID appropriate for the database type.
* - SQLite: returns nanoid (21-char alphanumeric)
* - PostgreSQL: returns UUID v4
*/
export function generateId(): string {
return nanoid(21);
return getDbType() === 'postgres' ? randomUUID() : nanoid(21);
}
export function generateTicketCode(): string {
return `TKT-${nanoid(8).toUpperCase()}`;
}
export function getNow(): string {
return new Date().toISOString();
/**
* Get current timestamp in the format appropriate for the database type.
* - SQLite: returns ISO string
* - PostgreSQL: returns Date object
*/
export function getNow(): string | Date {
const now = new Date();
return getDbType() === 'postgres' ? now : now.toISOString();
}
/**
* Convert a date value to the appropriate format for the database type.
* - SQLite: returns ISO string
* - PostgreSQL: returns Date object
*/
export function toDbDate(date: Date | string): string | Date {
const d = typeof date === 'string' ? new Date(date) : date;
return getDbType() === 'postgres' ? d : d.toISOString();
}
/**
* Convert a boolean value to the appropriate format for the database type.
* - SQLite: returns boolean (true/false) for mode: 'boolean'
* - PostgreSQL: returns integer (1/0) for pgInteger columns
*/
export function toDbBool(value: boolean): boolean | number {
return getDbType() === 'postgres' ? (value ? 1 : 0) : value;
}
/**
* Convert all boolean values in an object to the appropriate database format.
* Useful for converting request data before database insert/update.
*/
export function convertBooleansForDb<T extends Record<string, any>>(obj: T): T {
if (getDbType() !== 'postgres') {
return obj; // SQLite handles booleans automatically
}
const result = { ...obj };
for (const key in result) {
if (typeof result[key] === 'boolean') {
(result as any)[key] = result[key] ? 1 : 0;
}
}
return result;
}
export function formatCurrency(amount: number, currency: string = 'PYG'): string {

View File

@@ -1,5 +1,5 @@
import { Hono } from 'hono';
import { db, users, events, tickets, payments, contacts, emailSubscribers } from '../db/index.js';
import { db, dbGet, dbAll, users, events, tickets, payments, contacts, emailSubscribers } from '../db/index.js';
import { eq, and, gte, sql, desc } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js';
@@ -11,74 +11,84 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
const now = getNow();
// Get upcoming events
const upcomingEvents = await (db as any)
.select()
.from(events)
.where(
and(
eq((events as any).status, 'published'),
gte((events as any).startDatetime, now)
const upcomingEvents = await dbAll(
(db as any)
.select()
.from(events)
.where(
and(
eq((events as any).status, 'published'),
gte((events as any).startDatetime, now)
)
)
)
.orderBy((events as any).startDatetime)
.limit(5)
.all();
.orderBy((events as any).startDatetime)
.limit(5)
);
// Get recent tickets
const recentTickets = await (db as any)
.select()
.from(tickets)
.orderBy(desc((tickets as any).createdAt))
.limit(10)
.all();
const recentTickets = await dbAll(
(db as any)
.select()
.from(tickets)
.orderBy(desc((tickets as any).createdAt))
.limit(10)
);
// Get total stats
const totalUsers = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(users)
.get();
const totalUsers = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(users)
);
const totalEvents = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(events)
.get();
const totalEvents = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(events)
);
const totalTickets = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.get();
const totalTickets = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
);
const confirmedTickets = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(eq((tickets as any).status, 'confirmed'))
.get();
const confirmedTickets = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(eq((tickets as any).status, 'confirmed'))
);
const pendingPayments = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(payments)
.where(eq((payments as any).status, 'pending'))
.get();
const pendingPayments = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(payments)
.where(eq((payments as any).status, 'pending'))
);
const paidPayments = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).status, 'paid'))
.all();
const paidPayments = await dbAll<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).status, 'paid'))
);
const totalRevenue = paidPayments.reduce((sum: number, p: any) => sum + (p.amount || 0), 0);
const newContacts = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(contacts)
.where(eq((contacts as any).status, 'new'))
.get();
const newContacts = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(contacts)
.where(eq((contacts as any).status, 'new'))
);
const totalSubscribers = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(emailSubscribers)
.where(eq((emailSubscribers as any).status, 'active'))
.get();
const totalSubscribers = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(emailSubscribers)
.where(eq((emailSubscribers as any).status, 'active'))
);
return c.json({
dashboard: {
@@ -101,37 +111,40 @@ adminRouter.get('/dashboard', requireAuth(['admin', 'organizer']), async (c) =>
// Get analytics data (admin)
adminRouter.get('/analytics', requireAuth(['admin']), async (c) => {
// Get events with ticket counts
const allEvents = await (db as any).select().from(events).all();
const allEvents = await dbAll<any>((db as any).select().from(events));
const eventStats = await Promise.all(
allEvents.map(async (event: any) => {
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(eq((tickets as any).eventId, event.id))
.get();
const ticketCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(eq((tickets as any).eventId, event.id))
);
const confirmedCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed')
const confirmedCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed')
)
)
)
.get();
);
const checkedInCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'checked_in')
const checkedInCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'checked_in')
)
)
)
.get();
);
return {
id: event.id,
@@ -163,28 +176,31 @@ adminRouter.get('/export/tickets', requireAuth(['admin']), async (c) => {
query = query.where(eq((tickets as any).eventId, eventId));
}
const ticketList = await query.all();
const ticketList = await dbAll<any>(query);
// Get user and event details for each ticket
const enrichedTickets = await Promise.all(
ticketList.map(async (ticket: any) => {
const user = await (db as any)
.select()
.from(users)
.where(eq((users as any).id, ticket.userId))
.get();
const user = await dbGet<any>(
(db as any)
.select()
.from(users)
.where(eq((users as any).id, ticket.userId))
);
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
);
return {
ticketId: ticket.id,
@@ -215,24 +231,26 @@ adminRouter.get('/export/financial', requireAuth(['admin']), async (c) => {
// Get all payments
let query = (db as any).select().from(payments);
const allPayments = await query.all();
const allPayments = await dbAll<any>(query);
// Enrich with event and ticket data
const enrichedPayments = await Promise.all(
allPayments.map(async (payment: any) => {
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
);
if (!ticket) return null;
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
// Apply filters
if (eventId && ticket.eventId !== eventId) return null;

View File

@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, users, magicLinkTokens, User } from '../db/index.js';
import { db, dbGet, users, magicLinkTokens, User } from '../db/index.js';
import { eq } from 'drizzle-orm';
import {
hashPassword,
@@ -16,7 +16,7 @@ import {
invalidateAllUserSessions,
requireAuth,
} from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
import { generateId, getNow, toDbBool } from '../lib/utils.js';
import { sendEmail } from '../lib/email.js';
// User type that includes all fields (some added in schema updates)
@@ -121,7 +121,9 @@ auth.post('/register', zValidator('json', registerSchema), async (c) => {
}
// Check if email exists
const existing = await (db as any).select().from(users).where(eq((users as any).email, data.email)).get();
const existing = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).email, data.email))
);
if (existing) {
// If user exists but is unclaimed, allow claiming
if (!existing.isClaimed || existing.accountStatus === 'unclaimed') {
@@ -149,7 +151,7 @@ auth.post('/register', zValidator('json', registerSchema), async (c) => {
phone: data.phone || null,
role: firstUser ? 'admin' : 'user',
languagePreference: data.languagePreference || null,
isClaimed: true,
isClaimed: toDbBool(true),
googleId: null,
rucNumber: null,
accountStatus: 'active',
@@ -189,7 +191,9 @@ auth.post('/login', zValidator('json', loginSchema), async (c) => {
}, 429);
}
const user = await (db as any).select().from(users).where(eq((users as any).email, data.email)).get();
const user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).email, data.email))
);
if (!user) {
recordFailedAttempt(data.email);
return c.json({ error: 'Invalid credentials' }, 401);
@@ -243,7 +247,9 @@ auth.post('/login', zValidator('json', loginSchema), async (c) => {
auth.post('/magic-link/request', zValidator('json', magicLinkRequestSchema), async (c) => {
const { email } = c.req.valid('json');
const user = await (db as any).select().from(users).where(eq((users as any).email, email)).get();
const user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).email, email))
);
if (!user) {
// Don't reveal if email exists
@@ -288,7 +294,9 @@ auth.post('/magic-link/verify', zValidator('json', magicLinkVerifySchema), async
return c.json({ error: verification.error }, 400);
}
const user = await (db as any).select().from(users).where(eq((users as any).id, verification.userId)).get();
const user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).id, verification.userId))
);
if (!user || user.accountStatus === 'suspended') {
return c.json({ error: 'Invalid token' }, 400);
@@ -317,7 +325,9 @@ auth.post('/magic-link/verify', zValidator('json', magicLinkVerifySchema), async
auth.post('/password-reset/request', zValidator('json', passwordResetRequestSchema), async (c) => {
const { email } = c.req.valid('json');
const user = await (db as any).select().from(users).where(eq((users as any).email, email)).get();
const user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).email, email))
);
if (!user) {
// Don't reveal if email exists
@@ -389,7 +399,9 @@ auth.post('/password-reset/confirm', zValidator('json', passwordResetSchema), as
auth.post('/claim-account/request', zValidator('json', magicLinkRequestSchema), async (c) => {
const { email } = c.req.valid('json');
const user = await (db as any).select().from(users).where(eq((users as any).email, email)).get();
const user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).email, email))
);
if (!user) {
return c.json({ message: 'If an unclaimed account exists with this email, a claim link has been sent.' });
@@ -439,7 +451,7 @@ auth.post('/claim-account/confirm', zValidator('json', claimAccountSchema), asyn
const now = getNow();
const updates: Record<string, any> = {
isClaimed: true,
isClaimed: toDbBool(true),
accountStatus: 'active',
updatedAt: now,
};
@@ -461,7 +473,9 @@ auth.post('/claim-account/confirm', zValidator('json', claimAccountSchema), asyn
.set(updates)
.where(eq((users as any).id, verification.userId));
const user = await (db as any).select().from(users).where(eq((users as any).id, verification.userId)).get();
const user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).id, verification.userId))
);
const authToken = await createToken(user.id, user.email, user.role);
const refreshToken = await createRefreshToken(user.id);
@@ -510,11 +524,15 @@ auth.post('/google', zValidator('json', googleAuthSchema), async (c) => {
const { sub: googleId, email, name } = googleData;
// Check if user exists by email or google_id
let user = await (db as any).select().from(users).where(eq((users as any).email, email)).get();
let user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).email, email))
);
if (!user) {
// Check by google_id
user = await (db as any).select().from(users).where(eq((users as any).googleId, googleId)).get();
user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).googleId, googleId))
);
}
const now = getNow();
@@ -530,7 +548,7 @@ auth.post('/google', zValidator('json', googleAuthSchema), async (c) => {
.update(users)
.set({
googleId,
isClaimed: true,
isClaimed: toDbBool(true),
accountStatus: 'active',
updatedAt: now,
})
@@ -538,7 +556,9 @@ auth.post('/google', zValidator('json', googleAuthSchema), async (c) => {
}
// Refresh user data
user = await (db as any).select().from(users).where(eq((users as any).id, user.id)).get();
user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).id, user.id))
);
} else {
// Create new user
const firstUser = await isFirstUser();
@@ -552,7 +572,7 @@ auth.post('/google', zValidator('json', googleAuthSchema), async (c) => {
phone: null,
role: firstUser ? 'admin' : 'user',
languagePreference: null,
isClaimed: true,
isClaimed: toDbBool(true),
googleId,
rucNumber: null,
accountStatus: 'active',

View File

@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, contacts, emailSubscribers } from '../db/index.js';
import { db, dbGet, dbAll, contacts, emailSubscribers } from '../db/index.js';
import { eq, desc } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
@@ -48,11 +48,12 @@ contactsRouter.post('/subscribe', zValidator('json', subscribeSchema), async (c)
const data = c.req.valid('json');
// Check if already subscribed
const existing = await (db as any)
.select()
.from(emailSubscribers)
.where(eq((emailSubscribers as any).email, data.email))
.get();
const existing = await dbGet<any>(
(db as any)
.select()
.from(emailSubscribers)
.where(eq((emailSubscribers as any).email, data.email))
);
if (existing) {
if (existing.status === 'unsubscribed') {
@@ -87,11 +88,9 @@ contactsRouter.post('/subscribe', zValidator('json', subscribeSchema), async (c)
contactsRouter.post('/unsubscribe', zValidator('json', z.object({ email: z.string().email() })), async (c) => {
const { email } = c.req.valid('json');
const existing = await (db as any)
.select()
.from(emailSubscribers)
.where(eq((emailSubscribers as any).email, email))
.get();
const existing = await dbGet<any>(
(db as any).select().from(emailSubscribers).where(eq((emailSubscribers as any).email, email))
);
if (!existing) {
return c.json({ error: 'Email not found' }, 404);
@@ -115,7 +114,7 @@ contactsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(eq((contacts as any).status, status));
}
const result = await query.orderBy(desc((contacts as any).createdAt)).all();
const result = await dbAll(query.orderBy(desc((contacts as any).createdAt)));
return c.json({ contacts: result });
});
@@ -124,11 +123,12 @@ contactsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
contactsRouter.get('/:id', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
const contact = await (db as any)
.select()
.from(contacts)
.where(eq((contacts as any).id, id))
.get();
const contact = await dbGet(
(db as any)
.select()
.from(contacts)
.where(eq((contacts as any).id, id))
);
if (!contact) {
return c.json({ error: 'Contact not found' }, 404);
@@ -142,11 +142,9 @@ contactsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
const id = c.req.param('id');
const data = c.req.valid('json');
const existing = await (db as any)
.select()
.from(contacts)
.where(eq((contacts as any).id, id))
.get();
const existing = await dbGet<any>(
(db as any).select().from(contacts).where(eq((contacts as any).id, id))
);
if (!existing) {
return c.json({ error: 'Contact not found' }, 404);
@@ -157,11 +155,9 @@ contactsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
.set({ status: data.status })
.where(eq((contacts as any).id, id));
const updated = await (db as any)
.select()
.from(contacts)
.where(eq((contacts as any).id, id))
.get();
const updated = await dbGet<any>(
(db as any).select().from(contacts).where(eq((contacts as any).id, id))
);
return c.json({ contact: updated });
});
@@ -185,7 +181,7 @@ contactsRouter.get('/subscribers/list', requireAuth(['admin', 'marketing']), asy
query = query.where(eq((emailSubscribers as any).status, status));
}
const result = await query.orderBy(desc((emailSubscribers as any).createdAt)).all();
const result = await dbAll(query.orderBy(desc((emailSubscribers as any).createdAt)));
return c.json({ subscribers: result });
});

View File

@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, users, tickets, payments, events, invoices, User } from '../db/index.js';
import { db, dbGet, dbAll, users, tickets, payments, events, invoices, User } from '../db/index.js';
import { eq, desc, and, gt, sql } from 'drizzle-orm';
import { requireAuth, getUserSessions, invalidateSession, invalidateAllUserSessions, hashPassword, validatePassword } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
@@ -70,11 +70,12 @@ dashboard.put('/profile', zValidator('json', updateProfileSchema), async (c) =>
})
.where(eq((users as any).id, user.id));
const updatedUser = await (db as any)
.select()
.from(users)
.where(eq((users as any).id, user.id))
.get();
const updatedUser = await dbGet<any>(
(db as any)
.select()
.from(users)
.where(eq((users as any).id, user.id))
);
return c.json({
profile: {
@@ -95,36 +96,40 @@ dashboard.put('/profile', zValidator('json', updateProfileSchema), async (c) =>
dashboard.get('/tickets', async (c) => {
const user = (c as any).get('user') as AuthUser;
const userTickets = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, user.id))
.orderBy(desc((tickets as any).createdAt))
.all();
const userTickets = await dbAll<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, user.id))
.orderBy(desc((tickets as any).createdAt))
);
// Get event details for each ticket
const ticketsWithEvents = await Promise.all(
userTickets.map(async (ticket: any) => {
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
);
// Check for invoice
let invoice = null;
let invoice: any = null;
if (payment && payment.status === 'paid') {
invoice = await (db as any)
.select()
.from(invoices)
.where(eq((invoices as any).paymentId, payment.id))
.get();
invoice = await dbGet<any>(
(db as any)
.select()
.from(invoices)
.where(eq((invoices as any).paymentId, payment.id))
);
}
return {
@@ -168,40 +173,44 @@ dashboard.get('/tickets/:id', async (c) => {
const user = (c as any).get('user') as AuthUser;
const ticketId = c.req.param('id');
const ticket = await (db as any)
.select()
.from(tickets)
.where(
and(
eq((tickets as any).id, ticketId),
eq((tickets as any).userId, user.id)
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(
and(
eq((tickets as any).id, ticketId),
eq((tickets as any).userId, user.id)
)
)
)
.get();
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
}
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
);
let invoice = null;
if (payment && payment.status === 'paid') {
invoice = await (db as any)
.select()
.from(invoices)
.where(eq((invoices as any).paymentId, payment.id))
.get();
invoice = await dbGet(
(db as any)
.select()
.from(invoices)
.where(eq((invoices as any).paymentId, payment.id))
);
}
return c.json({
@@ -222,11 +231,12 @@ dashboard.get('/next-event', async (c) => {
const now = getNow();
// Get user's tickets for upcoming events
const userTickets = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, user.id))
.all();
const userTickets = await dbAll<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, user.id))
);
if (userTickets.length === 0) {
return c.json({ nextEvent: null });
@@ -240,11 +250,12 @@ dashboard.get('/next-event', async (c) => {
for (const ticket of userTickets) {
if (ticket.status === 'cancelled') continue;
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
if (!event) continue;
@@ -253,11 +264,12 @@ dashboard.get('/next-event', async (c) => {
if (!nextEvent || new Date(event.startDatetime) < new Date(nextEvent.startDatetime)) {
nextEvent = event;
nextTicket = ticket;
nextPayment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
.get();
nextPayment = await dbGet(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
);
}
}
}
@@ -282,11 +294,12 @@ dashboard.get('/payments', async (c) => {
const user = (c as any).get('user') as AuthUser;
// Get all user's tickets first
const userTickets = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, user.id))
.all();
const userTickets = await dbAll<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, user.id))
);
const ticketIds = userTickets.map((t: any) => t.id);
@@ -297,29 +310,32 @@ dashboard.get('/payments', async (c) => {
// Get all payments for user's tickets
const allPayments = [];
for (const ticketId of ticketIds) {
const ticketPayments = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
.all();
const ticketPayments = await dbAll<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
);
for (const payment of ticketPayments) {
const ticket = userTickets.find((t: any) => t.id === payment.ticketId);
const event = ticket
? await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get()
? await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
)
: null;
let invoice = null;
let invoice: any = null;
if (payment.status === 'paid') {
invoice = await (db as any)
.select()
.from(invoices)
.where(eq((invoices as any).paymentId, payment.id))
.get();
invoice = await dbGet<any>(
(db as any)
.select()
.from(invoices)
.where(eq((invoices as any).paymentId, payment.id))
);
}
allPayments.push({
@@ -355,36 +371,40 @@ dashboard.get('/payments', async (c) => {
dashboard.get('/invoices', async (c) => {
const user = (c as any).get('user') as AuthUser;
const userInvoices = await (db as any)
.select()
.from(invoices)
.where(eq((invoices as any).userId, user.id))
.orderBy(desc((invoices as any).createdAt))
.all();
const userInvoices = await dbAll<any>(
(db as any)
.select()
.from(invoices)
.where(eq((invoices as any).userId, user.id))
.orderBy(desc((invoices as any).createdAt))
);
// Get payment and event details for each invoice
const invoicesWithDetails = await Promise.all(
userInvoices.map(async (invoice: any) => {
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, invoice.paymentId))
.get();
let event = null;
if (payment) {
const ticket = await (db as any)
const payment = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
.from(payments)
.where(eq((payments as any).id, invoice.paymentId))
);
let event: any = null;
if (payment) {
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
);
if (ticket) {
event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
}
}
@@ -511,11 +531,12 @@ dashboard.get('/summary', async (c) => {
const membershipDays = Math.floor((now.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24));
// Get ticket count
const userTickets = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, user.id))
.all();
const userTickets = await dbAll<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, user.id))
);
const totalTickets = userTickets.length;
const confirmedTickets = userTickets.filter((t: any) => t.status === 'confirmed' || t.status === 'checked_in').length;
@@ -524,11 +545,12 @@ dashboard.get('/summary', async (c) => {
for (const ticket of userTickets) {
if (ticket.status === 'cancelled') continue;
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
if (event && new Date(event.startDatetime) > now) {
upcomingTickets.push({ ticket, event });
@@ -540,16 +562,17 @@ dashboard.get('/summary', async (c) => {
let pendingPayments = 0;
for (const ticketId of ticketIds) {
const payment = await (db as any)
.select()
.from(payments)
.where(
and(
eq((payments as any).ticketId, ticketId),
eq((payments as any).status, 'pending_approval')
const payment = await dbGet(
(db as any)
.select()
.from(payments)
.where(
and(
eq((payments as any).ticketId, ticketId),
eq((payments as any).status, 'pending_approval')
)
)
)
.get();
);
if (payment) pendingPayments++;
}

View File

@@ -1,9 +1,8 @@
import { Hono } from 'hono';
import { db, emailTemplates, emailLogs, events, tickets } from '../db/index.js';
import { db, dbGet, dbAll, emailTemplates, emailLogs, events, tickets } from '../db/index.js';
import { eq, desc, and, sql } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js';
import { nanoid } from 'nanoid';
import { getNow, generateId } from '../lib/utils.js';
import emailService from '../lib/email.js';
import { getTemplateVariables, defaultTemplates } from '../lib/emailTemplates.js';
@@ -13,11 +12,9 @@ const emailsRouter = new Hono();
// Get all email templates
emailsRouter.get('/templates', requireAuth(['admin', 'organizer']), async (c) => {
const templates = await (db as any)
.select()
.from(emailTemplates)
.orderBy(desc((emailTemplates as any).createdAt))
.all();
const templates = await dbAll<any>(
(db as any).select().from(emailTemplates).orderBy(desc((emailTemplates as any).createdAt))
);
// Parse variables JSON for each template
const parsedTemplates = templates.map((t: any) => ({
@@ -34,11 +31,12 @@ emailsRouter.get('/templates', requireAuth(['admin', 'organizer']), async (c) =>
emailsRouter.get('/templates/:id', requireAuth(['admin', 'organizer']), async (c) => {
const { id } = c.req.param();
const template = await (db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).id, id))
.get();
const template = await dbGet<any>(
(db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).id, id))
);
if (!template) {
return c.json({ error: 'Template not found' }, 404);
@@ -64,11 +62,9 @@ emailsRouter.post('/templates', requireAuth(['admin']), async (c) => {
}
// Check if slug already exists
const existing = await (db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).slug, slug))
.get();
const existing = await dbGet<any>(
(db as any).select().from(emailTemplates).where(eq((emailTemplates as any).slug, slug))
);
if (existing) {
return c.json({ error: 'Template with this slug already exists' }, 400);
@@ -76,7 +72,7 @@ emailsRouter.post('/templates', requireAuth(['admin']), async (c) => {
const now = getNow();
const template = {
id: nanoid(),
id: generateId(),
name,
slug,
subject,
@@ -111,11 +107,12 @@ emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
const { id } = c.req.param();
const body = await c.req.json();
const existing = await (db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).id, id))
.get();
const existing = await dbGet<any>(
(db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).id, id))
);
if (!existing) {
return c.json({ error: 'Template not found' }, 404);
@@ -148,11 +145,12 @@ emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
.set(updateData)
.where(eq((emailTemplates as any).id, id));
const updated = await (db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).id, id))
.get();
const updated = await dbGet<any>(
(db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).id, id))
);
return c.json({
template: {
@@ -169,11 +167,9 @@ emailsRouter.put('/templates/:id', requireAuth(['admin']), async (c) => {
emailsRouter.delete('/templates/:id', requireAuth(['admin']), async (c) => {
const { id } = c.req.param();
const template = await (db as any)
.select()
.from(emailTemplates)
.where(eq((emailTemplates as any).id, id))
.get();
const template = await dbGet<any>(
(db as any).select().from(emailTemplates).where(eq((emailTemplates as any).id, id))
);
if (!template) {
return c.json({ error: 'Template not found' }, 404);
@@ -306,11 +302,12 @@ emailsRouter.get('/logs', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(and(...conditions));
}
const logs = await query
.orderBy(desc((emailLogs as any).createdAt))
.limit(limit)
.offset(offset)
.all();
const logs = await dbAll(
query
.orderBy(desc((emailLogs as any).createdAt))
.limit(limit)
.offset(offset)
);
// Get total count
let countQuery = (db as any)
@@ -321,7 +318,7 @@ emailsRouter.get('/logs', requireAuth(['admin', 'organizer']), async (c) => {
countQuery = countQuery.where(and(...conditions));
}
const totalResult = await countQuery.get();
const totalResult = await dbGet<any>(countQuery);
const total = totalResult?.count || 0;
return c.json({
@@ -339,11 +336,9 @@ emailsRouter.get('/logs', requireAuth(['admin', 'organizer']), async (c) => {
emailsRouter.get('/logs/:id', requireAuth(['admin', 'organizer']), async (c) => {
const { id } = c.req.param();
const log = await (db as any)
.select()
.from(emailLogs)
.where(eq((emailLogs as any).id, id))
.get();
const log = await dbGet<any>(
(db as any).select().from(emailLogs).where(eq((emailLogs as any).id, id))
);
if (!log) {
return c.json({ error: 'Email log not found' }, 404);
@@ -362,22 +357,22 @@ emailsRouter.get('/stats', requireAuth(['admin', 'organizer']), async (c) => {
? (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(baseCondition)
: (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs);
const total = (await totalQuery.get())?.count || 0;
const total = (await dbGet<any>(totalQuery))?.count || 0;
const sentCondition = baseCondition
? and(baseCondition, eq((emailLogs as any).status, 'sent'))
: eq((emailLogs as any).status, 'sent');
const sent = (await (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(sentCondition).get())?.count || 0;
const sent = (await dbGet<any>((db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(sentCondition)))?.count || 0;
const failedCondition = baseCondition
? and(baseCondition, eq((emailLogs as any).status, 'failed'))
: eq((emailLogs as any).status, 'failed');
const failed = (await (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(failedCondition).get())?.count || 0;
const failed = (await dbGet<any>((db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(failedCondition)))?.count || 0;
const pendingCondition = baseCondition
? and(baseCondition, eq((emailLogs as any).status, 'pending'))
: eq((emailLogs as any).status, 'pending');
const pending = (await (db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(pendingCondition).get())?.count || 0;
const pending = (await dbGet<any>((db as any).select({ count: sql<number>`count(*)` }).from(emailLogs).where(pendingCondition)))?.count || 0;
return c.json({
stats: {

View File

@@ -1,10 +1,10 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, events, tickets, payments, eventPaymentOverrides, emailLogs, invoices } from '../db/index.js';
import { db, dbGet, dbAll, events, tickets, payments, eventPaymentOverrides, emailLogs, invoices } from '../db/index.js';
import { eq, desc, and, gte, sql } from 'drizzle-orm';
import { requireAuth, getAuthUser } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
import { generateId, getNow, convertBooleansForDb, toDbDate } from '../lib/utils.js';
interface UserContext {
id: string;
@@ -15,6 +15,21 @@ interface UserContext {
const eventsRouter = new Hono<{ Variables: { user: UserContext } }>();
// Helper to normalize event data for API response
// PostgreSQL decimal returns strings, booleans are stored as integers
function normalizeEvent(event: any) {
if (!event) return event;
return {
...event,
// Convert price from string/decimal to clean number
price: typeof event.price === 'string' ? parseFloat(event.price) : Number(event.price),
// Convert capacity from string to number if needed
capacity: typeof event.capacity === 'string' ? parseInt(event.capacity, 10) : Number(event.capacity),
// Convert boolean integers to actual booleans for frontend
externalBookingEnabled: Boolean(event.externalBookingEnabled),
};
}
// Custom validation error handler
const validationHook = (result: any, c: any) => {
if (!result.success) {
@@ -23,6 +38,27 @@ const validationHook = (result: any, c: any) => {
}
};
// Helper to parse price from string (handles both "45000" and "41,44" formats)
const parsePrice = (val: unknown): number => {
if (typeof val === 'number') return val;
if (typeof val === 'string') {
// Replace comma with dot for decimal parsing (European format)
const normalized = val.replace(',', '.');
const parsed = parseFloat(normalized);
return isNaN(parsed) ? 0 : parsed;
}
return 0;
};
// Helper to normalize boolean (handles true/false and 0/1)
const normalizeBoolean = (val: unknown): boolean => {
if (typeof val === 'boolean') return val;
if (typeof val === 'number') return val !== 0;
if (val === 'true') return true;
if (val === 'false') return false;
return false;
};
const baseEventSchema = z.object({
title: z.string().min(1),
titleEs: z.string().optional().nullable(),
@@ -34,14 +70,15 @@ const baseEventSchema = z.object({
endDatetime: z.string().optional().nullable(),
location: z.string().min(1),
locationUrl: z.string().url().optional().nullable().or(z.literal('')),
price: z.number().min(0).default(0),
// Accept price as number or string (handles "45000" and "41,44" formats)
price: z.union([z.number(), z.string()]).transform(parsePrice).pipe(z.number().min(0)).default(0),
currency: z.string().default('PYG'),
capacity: z.number().min(1).default(50),
capacity: z.union([z.number(), z.string()]).transform((val) => typeof val === 'string' ? parseInt(val, 10) || 50 : val).pipe(z.number().min(1)).default(50),
status: z.enum(['draft', 'published', 'cancelled', 'completed', 'archived']).default('draft'),
// Accept relative paths (/uploads/...) or full URLs
bannerUrl: z.string().optional().nullable().or(z.literal('')),
// External booking support
externalBookingEnabled: z.boolean().default(false),
// External booking support - accept boolean or number (0/1 from DB)
externalBookingEnabled: z.union([z.boolean(), z.number()]).transform(normalizeBoolean).default(false),
externalBookingUrl: z.string().url().optional().nullable().or(z.literal('')),
});
@@ -94,26 +131,28 @@ eventsRouter.get('/', async (c) => {
);
}
const result = await query.orderBy(desc((events as any).startDatetime)).all();
const result = await dbAll(query.orderBy(desc((events as any).startDatetime)));
// Get ticket counts for each event
const eventsWithCounts = await Promise.all(
result.map(async (event: any) => {
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed')
const ticketCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed')
)
)
)
.get();
);
const normalized = normalizeEvent(event);
return {
...event,
...normalized,
bookedCount: ticketCount?.count || 0,
availableSeats: event.capacity - (ticketCount?.count || 0),
availableSeats: normalized.capacity - (ticketCount?.count || 0),
};
})
);
@@ -125,29 +164,33 @@ eventsRouter.get('/', async (c) => {
eventsRouter.get('/:id', async (c) => {
const id = c.req.param('id');
const event = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
const event = await dbGet<any>(
(db as any).select().from(events).where(eq((events as any).id, id))
);
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Get ticket count
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, id),
eq((tickets as any).status, 'confirmed')
const ticketCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, id),
eq((tickets as any).status, 'confirmed')
)
)
)
.get();
);
const normalized = normalizeEvent(event);
return c.json({
event: {
...event,
...normalized,
bookedCount: ticketCount?.count || 0,
availableSeats: event.capacity - (ticketCount?.count || 0),
availableSeats: normalized.capacity - (ticketCount?.count || 0),
},
});
});
@@ -156,39 +199,42 @@ eventsRouter.get('/:id', async (c) => {
eventsRouter.get('/next/upcoming', async (c) => {
const now = getNow();
const event = await (db as any)
.select()
.from(events)
.where(
and(
eq((events as any).status, 'published'),
gte((events as any).startDatetime, now)
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(
and(
eq((events as any).status, 'published'),
gte((events as any).startDatetime, now)
)
)
)
.orderBy((events as any).startDatetime)
.limit(1)
.get();
.orderBy((events as any).startDatetime)
.limit(1)
);
if (!event) {
return c.json({ event: null });
}
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed')
const ticketCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, event.id),
eq((tickets as any).status, 'confirmed')
)
)
)
.get();
);
const normalized = normalizeEvent(event);
return c.json({
event: {
...event,
...normalized,
bookedCount: ticketCount?.count || 0,
availableSeats: event.capacity - (ticketCount?.count || 0),
availableSeats: normalized.capacity - (ticketCount?.count || 0),
},
});
});
@@ -200,16 +246,22 @@ eventsRouter.post('/', requireAuth(['admin', 'organizer']), zValidator('json', c
const now = getNow();
const id = generateId();
// Convert data for database compatibility
const dbData = convertBooleansForDb(data);
const newEvent = {
id,
...data,
...dbData,
startDatetime: toDbDate(data.startDatetime),
endDatetime: data.endDatetime ? toDbDate(data.endDatetime) : null,
createdAt: now,
updatedAt: now,
};
await (db as any).insert(events).values(newEvent);
return c.json({ event: newEvent }, 201);
// Return normalized event data
return c.json({ event: normalizeEvent(newEvent) }, 201);
});
// Update event (admin/organizer only)
@@ -217,46 +269,64 @@ eventsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json',
const id = c.req.param('id');
const data = c.req.valid('json');
const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
const existing = await dbGet(
(db as any).select().from(events).where(eq((events as any).id, id))
);
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
const now = getNow();
// Convert data for database compatibility
const updateData: Record<string, any> = { ...convertBooleansForDb(data), updatedAt: now };
// Convert datetime fields if present
if (data.startDatetime) {
updateData.startDatetime = toDbDate(data.startDatetime);
}
if (data.endDatetime !== undefined) {
updateData.endDatetime = data.endDatetime ? toDbDate(data.endDatetime) : null;
}
await (db as any)
.update(events)
.set({ ...data, updatedAt: now })
.set(updateData)
.where(eq((events as any).id, id));
const updated = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
const updated = await dbGet(
(db as any).select().from(events).where(eq((events as any).id, id))
);
return c.json({ event: updated });
return c.json({ event: normalizeEvent(updated) });
});
// Delete event (admin only)
eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
const id = c.req.param('id');
const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
const existing = await dbGet(
(db as any).select().from(events).where(eq((events as any).id, id))
);
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
// Get all tickets for this event
const eventTickets = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).eventId, id))
.all();
const eventTickets = await dbAll<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).eventId, id))
);
// Delete invoices and payments for all tickets of this event
for (const ticket of eventTickets) {
// Get payments for this ticket
const ticketPayments = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
.all();
const ticketPayments = await dbAll<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticket.id))
);
// Delete invoices for each payment
for (const payment of ticketPayments) {
@@ -289,11 +359,12 @@ eventsRouter.delete('/:id', requireAuth(['admin']), async (c) => {
eventsRouter.get('/:id/attendees', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
const id = c.req.param('id');
const attendees = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).eventId, id))
.all();
const attendees = await dbAll(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).eventId, id))
);
return c.json({ attendees });
});
@@ -302,7 +373,9 @@ eventsRouter.get('/:id/attendees', requireAuth(['admin', 'organizer', 'staff']),
eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
const existing = await (db as any).select().from(events).where(eq((events as any).id, id)).get();
const existing = await dbGet<any>(
(db as any).select().from(events).where(eq((events as any).id, id))
);
if (!existing) {
return c.json({ error: 'Event not found' }, 404);
}
@@ -319,7 +392,7 @@ eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (
descriptionEs: existing.descriptionEs,
shortDescription: existing.shortDescription,
shortDescriptionEs: existing.shortDescriptionEs,
startDatetime: existing.startDatetime,
startDatetime: existing.startDatetime, // Already in DB format from existing record
endDatetime: existing.endDatetime,
location: existing.location,
locationUrl: existing.locationUrl,
@@ -328,7 +401,7 @@ eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (
capacity: existing.capacity,
status: 'draft',
bannerUrl: existing.bannerUrl,
externalBookingEnabled: existing.externalBookingEnabled || false,
externalBookingEnabled: existing.externalBookingEnabled ?? 0, // Already in DB format (0/1)
externalBookingUrl: existing.externalBookingUrl,
createdAt: now,
updatedAt: now,
@@ -336,7 +409,7 @@ eventsRouter.post('/:id/duplicate', requireAuth(['admin', 'organizer']), async (
await (db as any).insert(events).values(duplicatedEvent);
return c.json({ event: duplicatedEvent, message: 'Event duplicated successfully' }, 201);
return c.json({ event: normalizeEvent(duplicatedEvent), message: 'Event duplicated successfully' }, 201);
});
export default eventsRouter;

View File

@@ -0,0 +1,393 @@
import { Hono } from 'hono';
import { db, dbGet, dbAll, legalPages } from '../db/index.js';
import { eq, desc } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow, generateId } from '../lib/utils.js';
import fs from 'fs';
import path from 'path';
const legalPagesRouter = new Hono();
// Helper: Convert plain text to simple markdown
// Preserves paragraphs and line breaks, nothing fancy
function textToMarkdown(text: string): string {
if (!text) return '';
// Split into paragraphs (double newlines)
const paragraphs = text.split(/\n\s*\n/);
// Process each paragraph
const processed = paragraphs.map(para => {
// Replace single newlines with double spaces + newline for markdown line breaks
return para.trim().replace(/\n/g, ' \n');
});
// Join paragraphs with double newlines
return processed.join('\n\n');
}
// Helper: Convert markdown to plain text for editing
function markdownToText(markdown: string): string {
if (!markdown) return '';
let text = markdown;
// Remove markdown heading markers (# ## ###)
text = text.replace(/^#{1,6}\s+/gm, '');
// Remove horizontal rules
text = text.replace(/^---+$/gm, '');
// Remove bold/italic markers
text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
text = text.replace(/\*([^*]+)\*/g, '$1');
text = text.replace(/__([^_]+)__/g, '$1');
text = text.replace(/_([^_]+)_/g, '$1');
// Remove list markers (preserve text)
text = text.replace(/^\s*[\*\-\+]\s+/gm, '');
text = text.replace(/^\s*\d+\.\s+/gm, '');
// Remove link formatting [text](url) -> text
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
// Remove double-space line breaks
text = text.replace(/ \n/g, '\n');
// Normalize multiple newlines
text = text.replace(/\n{3,}/g, '\n\n');
return text.trim();
}
// Helper: Extract title from markdown content
function extractTitleFromMarkdown(content: string): string {
if (!content) return 'Untitled';
const match = content.match(/^#\s+(.+?)(?:\s*[-]\s*.+)?$/m);
if (match) {
return match[1].trim();
}
// Fallback to first line
const firstLine = content.split('\n')[0].replace(/^#+\s*/, '').trim();
return firstLine || 'Untitled';
}
// Helper: Get legal directory path
function getLegalDir(): string {
// When running from backend, legal folder is in frontend
const possiblePaths = [
path.join(process.cwd(), '../frontend/legal'),
path.join(process.cwd(), 'frontend/legal'),
path.join(process.cwd(), 'legal'),
];
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
return p;
}
}
return possiblePaths[0]; // Default
}
// Helper: Convert filename to slug
function fileNameToSlug(fileName: string): string {
return fileName.replace('.md', '').replace(/_/g, '-');
}
// Title map for localization
const titleMap: Record<string, { en: string; es: string }> = {
'privacy-policy': { en: 'Privacy Policy', es: 'Política de Privacidad' },
'terms-policy': { en: 'Terms & Conditions', es: 'Términos y Condiciones' },
'refund-cancelation-policy': { en: 'Refund & Cancellation Policy', es: 'Política de Reembolso y Cancelación' },
};
// Helper: Get localized content with fallback
// If requested locale content is missing, fallback to the other locale
function getLocalizedContent(page: any, locale: string = 'en'): { title: string; contentMarkdown: string } {
const isSpanish = locale === 'es';
// Title: prefer requested locale, fallback to other
let title: string;
if (isSpanish) {
title = page.titleEs || page.title;
} else {
title = page.title || page.titleEs;
}
// Content: prefer requested locale, fallback to other
let contentMarkdown: string;
if (isSpanish) {
contentMarkdown = page.contentMarkdownEs || page.contentMarkdown;
} else {
contentMarkdown = page.contentMarkdown || page.contentMarkdownEs;
}
return { title, contentMarkdown };
}
// ==================== Public Routes ====================
// Get all legal pages (public, for footer/navigation)
legalPagesRouter.get('/', async (c) => {
const locale = c.req.query('locale') || 'en';
const pages = await dbAll<any>(
(db as any)
.select({
id: (legalPages as any).id,
slug: (legalPages as any).slug,
title: (legalPages as any).title,
titleEs: (legalPages as any).titleEs,
updatedAt: (legalPages as any).updatedAt,
})
.from(legalPages)
.orderBy((legalPages as any).slug)
);
// Return pages with localized title
const localizedPages = pages.map((page: any) => ({
id: page.id,
slug: page.slug,
title: locale === 'es' ? (page.titleEs || page.title) : (page.title || page.titleEs),
updatedAt: page.updatedAt,
}));
return c.json({ pages: localizedPages });
});
// Get single legal page (public, for rendering)
legalPagesRouter.get('/:slug', async (c) => {
const { slug } = c.req.param();
const locale = c.req.query('locale') || 'en';
// First try to get from database
const page = await dbGet<any>(
(db as any).select().from(legalPages).where(eq((legalPages as any).slug, slug))
);
if (page) {
// Get localized content with fallback
const { title, contentMarkdown } = getLocalizedContent(page, locale);
return c.json({
page: {
id: page.id,
slug: page.slug,
title,
contentMarkdown,
updatedAt: page.updatedAt,
source: 'database',
}
});
}
// Fallback to filesystem
const legalDir = getLegalDir();
const fileName = slug.replace(/-/g, '_') + '.md';
const filePath = path.join(legalDir, fileName);
if (fs.existsSync(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
const titles = titleMap[slug];
const title = locale === 'es'
? (titles?.es || titles?.en || slug)
: (titles?.en || titles?.es || slug);
return c.json({
page: {
slug,
title,
contentMarkdown: content,
source: 'filesystem',
}
});
}
return c.json({ error: 'Legal page not found' }, 404);
});
// ==================== Admin Routes ====================
// Get all legal pages for admin (with full content)
legalPagesRouter.get('/admin/list', requireAuth(['admin']), async (c) => {
const pages = await dbAll<any>(
(db as any)
.select()
.from(legalPages)
.orderBy((legalPages as any).slug)
);
// Add flags to indicate which languages have content
const pagesWithFlags = pages.map((page: any) => ({
...page,
hasEnglish: Boolean(page.contentText),
hasSpanish: Boolean(page.contentTextEs),
}));
return c.json({ pages: pagesWithFlags });
});
// Get single legal page for editing (admin)
legalPagesRouter.get('/admin/:slug', requireAuth(['admin']), async (c) => {
const { slug } = c.req.param();
const page = await dbGet<any>(
(db as any).select().from(legalPages).where(eq((legalPages as any).slug, slug))
);
if (!page) {
return c.json({ error: 'Legal page not found' }, 404);
}
return c.json({
page: {
...page,
hasEnglish: Boolean(page.contentText),
hasSpanish: Boolean(page.contentTextEs),
}
});
});
// Update legal page (admin only)
// Note: No creation or deletion - only updates are allowed
// Accepts markdown content from rich text editor
legalPagesRouter.put('/admin/:slug', requireAuth(['admin']), async (c) => {
const { slug } = c.req.param();
const user = (c as any).get('user');
const body = await c.req.json();
// Accept both contentText (legacy plain text) and contentMarkdown (from rich text editor)
const { contentText, contentTextEs, contentMarkdown, contentMarkdownEs, title, titleEs } = body;
// Determine content - prefer markdown if provided, fall back to contentText
const enContent = contentMarkdown !== undefined ? contentMarkdown : contentText;
const esContent = contentMarkdownEs !== undefined ? contentMarkdownEs : contentTextEs;
// At least one content field is required
if (!enContent && !esContent) {
return c.json({ error: 'At least one language content is required' }, 400);
}
const existing = await dbGet(
(db as any)
.select()
.from(legalPages)
.where(eq((legalPages as any).slug, slug))
);
if (!existing) {
return c.json({ error: 'Legal page not found' }, 404);
}
const updateData: any = {
updatedAt: getNow(),
updatedBy: user?.id || null,
};
// Update English content if provided
if (enContent !== undefined) {
// Store markdown directly (from rich text editor)
updateData.contentMarkdown = enContent;
// Derive plain text from markdown
updateData.contentText = markdownToText(enContent);
}
// Update Spanish content if provided
if (esContent !== undefined) {
updateData.contentMarkdownEs = esContent || null;
updateData.contentTextEs = esContent ? markdownToText(esContent) : null;
}
// Allow updating titles
if (title !== undefined) {
updateData.title = title;
}
if (titleEs !== undefined) {
updateData.titleEs = titleEs || null;
}
await (db as any)
.update(legalPages)
.set(updateData)
.where(eq((legalPages as any).slug, slug));
const updated = await dbGet<any>(
(db as any).select().from(legalPages).where(eq((legalPages as any).slug, slug))
);
return c.json({
page: {
...updated,
hasEnglish: Boolean(updated.contentText),
hasSpanish: Boolean(updated.contentTextEs),
},
message: 'Legal page updated successfully'
});
});
// Seed legal pages from filesystem (admin only)
// This imports markdown files and converts them to plain text for editing
// Files are imported as English content - Spanish can be added manually
legalPagesRouter.post('/admin/seed', requireAuth(['admin']), async (c) => {
const user = (c as any).get('user');
// Check if table already has data
const existingPages = await dbAll(
(db as any)
.select({ id: (legalPages as any).id })
.from(legalPages)
.limit(1)
);
if (existingPages.length > 0) {
return c.json({
message: 'Legal pages already seeded. Use update to modify pages.',
seeded: 0
});
}
const legalDir = getLegalDir();
if (!fs.existsSync(legalDir)) {
return c.json({ error: `Legal directory not found: ${legalDir}` }, 400);
}
const files = fs.readdirSync(legalDir).filter(f => f.endsWith('.md'));
const seededPages: string[] = [];
const now = getNow();
for (const file of files) {
const filePath = path.join(legalDir, file);
const contentMarkdown = fs.readFileSync(filePath, 'utf-8');
const slug = fileNameToSlug(file);
const contentText = markdownToText(contentMarkdown);
const titles = titleMap[slug];
const title = titles?.en || extractTitleFromMarkdown(contentMarkdown);
const titleEs = titles?.es || null;
await (db as any).insert(legalPages).values({
id: generateId(),
slug,
title,
titleEs,
contentText, // English plain text
contentTextEs: null, // Spanish to be added manually
contentMarkdown, // English markdown
contentMarkdownEs: null, // Spanish to be generated when contentTextEs is set
updatedAt: now,
updatedBy: user?.id || null,
createdAt: now,
});
seededPages.push(slug);
}
return c.json({
message: `Successfully seeded ${seededPages.length} legal pages (English content imported, Spanish can be added via editor)`,
seeded: seededPages.length,
pages: seededPages,
});
});
export default legalPagesRouter;

View File

@@ -1,6 +1,6 @@
import { Hono } from 'hono';
import { streamSSE } from 'hono/streaming';
import { db, tickets, payments } from '../db/index.js';
import { db, dbGet, tickets, payments } from '../db/index.js';
import { eq } from 'drizzle-orm';
import { getNow } from '../lib/utils.js';
import { verifyWebhookPayment, getPaymentStatus } from '../lib/lnbits.js';
@@ -157,11 +157,9 @@ async function handlePaymentComplete(ticketId: string, paymentHash: string) {
const now = getNow();
// Check if already confirmed to avoid duplicate updates
const existingTicket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
.get();
const existingTicket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, ticketId))
);
if (existingTicket?.status === 'confirmed') {
console.log(`Ticket ${ticketId} already confirmed, skipping update`);
@@ -188,11 +186,12 @@ async function handlePaymentComplete(ticketId: string, paymentHash: string) {
console.log(`Ticket ${ticketId} confirmed via Lightning payment (hash: ${paymentHash})`);
// Get payment for sending receipt
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
);
// Send confirmation emails asynchronously
Promise.all([
@@ -211,11 +210,9 @@ lnbitsRouter.get('/stream/:ticketId', async (c) => {
const ticketId = c.req.param('ticketId');
// Verify ticket exists
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
.get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, ticketId))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -227,11 +224,9 @@ lnbitsRouter.get('/stream/:ticketId', async (c) => {
}
// Get payment to start background checker
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
.get();
const payment = await dbGet<any>(
(db as any).select().from(payments).where(eq((payments as any).ticketId, ticketId))
);
// Start background checker if not already running
if (payment?.reference && !activeCheckers.has(ticketId)) {
@@ -290,21 +285,23 @@ lnbitsRouter.get('/stream/:ticketId', async (c) => {
lnbitsRouter.get('/status/:ticketId', async (c) => {
const ticketId = c.req.param('ticketId');
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
.get();
const ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, ticketId))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
}
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, ticketId))
);
return c.json({
ticketStatus: ticket.status,

View File

@@ -1,5 +1,5 @@
import { Hono } from 'hono';
import { db, media } from '../db/index.js';
import { db, dbGet, dbAll, media } from '../db/index.js';
import { eq } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
@@ -85,11 +85,9 @@ mediaRouter.post('/upload', requireAuth(['admin', 'organizer']), async (c) => {
mediaRouter.get('/:id', async (c) => {
const id = c.req.param('id');
const mediaRecord = await (db as any)
.select()
.from(media)
.where(eq((media as any).id, id))
.get();
const mediaRecord = await dbGet<any>(
(db as any).select().from(media).where(eq((media as any).id, id))
);
if (!mediaRecord) {
return c.json({ error: 'Media not found' }, 404);
@@ -102,11 +100,9 @@ mediaRouter.get('/:id', async (c) => {
mediaRouter.delete('/:id', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
const mediaRecord = await (db as any)
.select()
.from(media)
.where(eq((media as any).id, id))
.get();
const mediaRecord = await dbGet<any>(
(db as any).select().from(media).where(eq((media as any).id, id))
);
if (!mediaRecord) {
return c.json({ error: 'Media not found' }, 404);
@@ -142,7 +138,7 @@ mediaRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(eq((media as any).relatedId, relatedId));
}
const result = await query.all();
const result = await dbAll(query);
return c.json({ media: result });
});

View File

@@ -1,10 +1,10 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, paymentOptions, eventPaymentOverrides, events } from '../db/index.js';
import { db, dbGet, paymentOptions, eventPaymentOverrides, events } from '../db/index.js';
import { eq } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
import { generateId, getNow, convertBooleansForDb } from '../lib/utils.js';
const paymentOptionsRouter = new Hono();
@@ -52,10 +52,9 @@ const updateEventOverridesSchema = z.object({
// Get global payment options
paymentOptionsRouter.get('/', requireAuth(['admin']), async (c) => {
const options = await (db as any)
.select()
.from(paymentOptions)
.get();
const options = await dbGet<any>(
(db as any).select().from(paymentOptions)
);
// If no options exist yet, return defaults
if (!options) {
@@ -92,17 +91,21 @@ paymentOptionsRouter.put('/', requireAuth(['admin']), zValidator('json', updateP
const now = getNow();
// Check if options exist
const existing = await (db as any)
.select()
.from(paymentOptions)
.get();
const existing = await dbGet<any>(
(db as any)
.select()
.from(paymentOptions)
);
// Convert boolean fields for database compatibility
const dbData = convertBooleansForDb(data);
if (existing) {
// Update existing
await (db as any)
.update(paymentOptions)
.set({
...data,
...dbData,
updatedAt: now,
updatedBy: user.id,
})
@@ -112,16 +115,17 @@ paymentOptionsRouter.put('/', requireAuth(['admin']), zValidator('json', updateP
const id = generateId();
await (db as any).insert(paymentOptions).values({
id,
...data,
...dbData,
updatedAt: now,
updatedBy: user.id,
});
}
const updated = await (db as any)
.select()
.from(paymentOptions)
.get();
const updated = await dbGet(
(db as any)
.select()
.from(paymentOptions)
);
return c.json({ paymentOptions: updated, message: 'Payment options updated successfully' });
});
@@ -131,28 +135,31 @@ paymentOptionsRouter.get('/event/:eventId', async (c) => {
const eventId = c.req.param('eventId');
// Get the event first to verify it exists
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, eventId))
.get();
const event = await dbGet(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, eventId))
);
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Get global options
const globalOptions = await (db as any)
.select()
.from(paymentOptions)
.get();
const globalOptions = await dbGet<any>(
(db as any)
.select()
.from(paymentOptions)
);
// Get event overrides
const overrides = await (db as any)
.select()
.from(eventPaymentOverrides)
.where(eq((eventPaymentOverrides as any).eventId, eventId))
.get();
const overrides = await dbGet<any>(
(db as any)
.select()
.from(eventPaymentOverrides)
.where(eq((eventPaymentOverrides as any).eventId, eventId))
);
// Merge global with overrides (override takes precedence if not null)
const defaults = {
@@ -206,11 +213,9 @@ paymentOptionsRouter.get('/event/:eventId', async (c) => {
paymentOptionsRouter.get('/event/:eventId/overrides', requireAuth(['admin', 'organizer']), async (c) => {
const eventId = c.req.param('eventId');
const overrides = await (db as any)
.select()
.from(eventPaymentOverrides)
.where(eq((eventPaymentOverrides as any).eventId, eventId))
.get();
const overrides = await dbGet<any>(
(db as any).select().from(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, eventId))
);
return c.json({ overrides: overrides || null });
});
@@ -222,28 +227,27 @@ paymentOptionsRouter.put('/event/:eventId/overrides', requireAuth(['admin', 'org
const now = getNow();
// Verify event exists
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, eventId))
.get();
const event = await dbGet<any>(
(db as any).select().from(events).where(eq((events as any).id, eventId))
);
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Check if overrides exist
const existing = await (db as any)
.select()
.from(eventPaymentOverrides)
.where(eq((eventPaymentOverrides as any).eventId, eventId))
.get();
const existing = await dbGet<any>(
(db as any).select().from(eventPaymentOverrides).where(eq((eventPaymentOverrides as any).eventId, eventId))
);
// Convert boolean fields for database compatibility
const dbData = convertBooleansForDb(data);
if (existing) {
await (db as any)
.update(eventPaymentOverrides)
.set({
...data,
...dbData,
updatedAt: now,
})
.where(eq((eventPaymentOverrides as any).id, existing.id));
@@ -252,17 +256,18 @@ paymentOptionsRouter.put('/event/:eventId/overrides', requireAuth(['admin', 'org
await (db as any).insert(eventPaymentOverrides).values({
id,
eventId,
...data,
...dbData,
createdAt: now,
updatedAt: now,
});
}
const updated = await (db as any)
.select()
.from(eventPaymentOverrides)
.where(eq((eventPaymentOverrides as any).eventId, eventId))
.get();
const updated = await dbGet(
(db as any)
.select()
.from(eventPaymentOverrides)
.where(eq((eventPaymentOverrides as any).eventId, eventId))
);
return c.json({ overrides: updated, message: 'Event payment overrides updated successfully' });
});

View File

@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, payments, tickets, events } from '../db/index.js';
import { db, dbGet, dbAll, payments, tickets, events } from '../db/index.js';
import { eq, desc, and, or, sql } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js';
@@ -30,11 +30,12 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => {
const pendingApproval = c.req.query('pendingApproval');
// Get all payments with their associated tickets
let allPayments = await (db as any)
.select()
.from(payments)
.orderBy(desc((payments as any).createdAt))
.all();
let allPayments = await dbAll<any>(
(db as any)
.select()
.from(payments)
.orderBy(desc((payments as any).createdAt))
);
// Filter by status
if (status) {
@@ -54,19 +55,21 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => {
// Enrich with ticket and event data
const enrichedPayments = await Promise.all(
allPayments.map(async (payment: any) => {
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
let event = null;
if (ticket) {
event = await (db as any)
const ticket = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
);
let event: any = null;
if (ticket) {
event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
}
return {
@@ -93,29 +96,32 @@ paymentsRouter.get('/', requireAuth(['admin']), async (c) => {
// Get payments pending approval (admin dashboard view)
paymentsRouter.get('/pending-approval', requireAuth(['admin', 'organizer']), async (c) => {
const pendingPayments = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).status, 'pending_approval'))
.orderBy(desc((payments as any).userMarkedPaidAt))
.all();
const pendingPayments = await dbAll<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).status, 'pending_approval'))
.orderBy(desc((payments as any).userMarkedPaidAt))
);
// Enrich with ticket and event data
const enrichedPayments = await Promise.all(
pendingPayments.map(async (payment: any) => {
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
let event = null;
if (ticket) {
event = await (db as any)
const ticket = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
);
let event: any = null;
if (ticket) {
event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
}
return {
@@ -144,22 +150,24 @@ paymentsRouter.get('/pending-approval', requireAuth(['admin', 'organizer']), asy
paymentsRouter.get('/:id', requireAuth(['admin', 'organizer']), async (c) => {
const id = c.req.param('id');
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
}
// Get associated ticket
const ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
.get();
const ticket = await dbGet(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, payment.ticketId))
);
return c.json({ payment: { ...payment, ticket } });
});
@@ -170,11 +178,12 @@ paymentsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
const data = c.req.valid('json');
const user = (c as any).get('user');
const existing = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const existing = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
if (!existing) {
return c.json({ error: 'Payment not found' }, 404);
@@ -211,11 +220,12 @@ paymentsRouter.put('/:id', requireAuth(['admin', 'organizer']), zValidator('json
});
}
const updated = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const updated = await dbGet(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
return c.json({ payment: updated });
});
@@ -226,11 +236,12 @@ paymentsRouter.post('/:id/approve', requireAuth(['admin', 'organizer']), zValida
const { adminNote } = c.req.valid('json');
const user = (c as any).get('user');
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -269,11 +280,12 @@ paymentsRouter.post('/:id/approve', requireAuth(['admin', 'organizer']), zValida
console.error('[Email] Failed to send confirmation emails:', err);
});
const updated = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const updated = await dbGet(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
return c.json({ payment: updated, message: 'Payment approved successfully' });
});
@@ -284,11 +296,12 @@ paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidat
const { adminNote } = c.req.valid('json');
const user = (c as any).get('user');
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -327,11 +340,12 @@ paymentsRouter.post('/:id/reject', requireAuth(['admin', 'organizer']), zValidat
});
}
const updated = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const updated = await dbGet(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
return c.json({ payment: updated, message: 'Payment rejected and booking cancelled' });
});
@@ -342,11 +356,12 @@ paymentsRouter.post('/:id/note', requireAuth(['admin', 'organizer']), async (c)
const body = await c.req.json();
const { adminNote } = body;
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -362,11 +377,12 @@ paymentsRouter.post('/:id/note', requireAuth(['admin', 'organizer']), async (c)
})
.where(eq((payments as any).id, id));
const updated = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const updated = await dbGet(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
return c.json({ payment: updated, message: 'Note updated' });
});
@@ -375,11 +391,12 @@ paymentsRouter.post('/:id/note', requireAuth(['admin', 'organizer']), async (c)
paymentsRouter.post('/:id/refund', requireAuth(['admin']), async (c) => {
const id = c.req.param('id');
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, id))
);
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -426,7 +443,7 @@ paymentsRouter.post('/webhook', async (c) => {
// Get payment statistics (admin)
paymentsRouter.get('/stats/overview', requireAuth(['admin']), async (c) => {
const allPayments = await (db as any).select().from(payments).all();
const allPayments = await dbAll<any>((db as any).select().from(payments));
const stats = {
total: allPayments.length,

View File

@@ -1,10 +1,10 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, siteSettings } from '../db/index.js';
import { db, dbGet, siteSettings } from '../db/index.js';
import { eq } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { generateId, getNow } from '../lib/utils.js';
import { generateId, getNow, toDbBool } from '../lib/utils.js';
interface UserContext {
id: string;
@@ -34,7 +34,9 @@ const updateSiteSettingsSchema = z.object({
// Get site settings (public - needed for frontend timezone)
siteSettingsRouter.get('/', async (c) => {
const settings = await (db as any).select().from(siteSettings).limit(1).get();
const settings = await dbGet(
(db as any).select().from(siteSettings).limit(1)
);
if (!settings) {
// Return default settings if none exist
@@ -95,7 +97,9 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
const now = getNow();
// Check if settings exist
const existing = await (db as any).select().from(siteSettings).limit(1).get();
const existing = await dbGet<any>(
(db as any).select().from(siteSettings).limit(1)
);
if (!existing) {
// Create new settings record
@@ -112,7 +116,7 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
instagramUrl: data.instagramUrl || null,
twitterUrl: data.twitterUrl || null,
linkedinUrl: data.linkedinUrl || null,
maintenanceMode: data.maintenanceMode || false,
maintenanceMode: toDbBool(data.maintenanceMode || false),
maintenanceMessage: data.maintenanceMessage || null,
maintenanceMessageEs: data.maintenanceMessageEs || null,
updatedAt: now,
@@ -125,18 +129,24 @@ siteSettingsRouter.put('/', requireAuth(['admin']), zValidator('json', updateSit
}
// Update existing settings
const updateData = {
const updateData: Record<string, any> = {
...data,
updatedAt: now,
updatedBy: user.id,
};
// Convert maintenanceMode boolean to appropriate format for database
if (typeof data.maintenanceMode === 'boolean') {
updateData.maintenanceMode = toDbBool(data.maintenanceMode);
}
await (db as any)
.update(siteSettings)
.set(updateData)
.where(eq((siteSettings as any).id, existing.id));
const updated = await (db as any).select().from(siteSettings).where(eq((siteSettings as any).id, existing.id)).get();
const updated = await dbGet(
(db as any).select().from(siteSettings).where(eq((siteSettings as any).id, existing.id))
);
return c.json({ settings: updated, message: 'Settings updated successfully' });
});

View File

@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, tickets, events, users, payments, paymentOptions } from '../db/index.js';
import { db, dbGet, dbAll, tickets, events, users, payments, paymentOptions } from '../db/index.js';
import { eq, and, sql } from 'drizzle-orm';
import { requireAuth, getAuthUser } from '../lib/auth.js';
import { generateId, generateTicketCode, getNow } from '../lib/utils.js';
@@ -47,7 +47,9 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
const data = c.req.valid('json');
// Get event
const event = await (db as any).select().from(events).where(eq((events as any).id, data.eventId)).get();
const event = await dbGet<any>(
(db as any).select().from(events).where(eq((events as any).id, data.eventId))
);
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
@@ -57,23 +59,26 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
}
// Check capacity
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, data.eventId),
eq((tickets as any).status, 'confirmed')
const ticketCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, data.eventId),
eq((tickets as any).status, 'confirmed')
)
)
)
.get();
);
if ((ticketCount?.count || 0) >= event.capacity) {
return c.json({ error: 'Event is sold out' }, 400);
}
// Find or create user
let user = await (db as any).select().from(users).where(eq((users as any).email, data.email)).get();
let user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).email, data.email))
);
const now = getNow();
@@ -98,24 +103,26 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
}
// Check for duplicate booking (unless allowDuplicateBookings is enabled)
const globalOptions = await (db as any)
.select()
.from(paymentOptions)
.get();
const globalOptions = await dbGet<any>(
(db as any)
.select()
.from(paymentOptions)
);
const allowDuplicateBookings = globalOptions?.allowDuplicateBookings ?? false;
if (!allowDuplicateBookings) {
const existingTicket = await (db as any)
.select()
.from(tickets)
.where(
and(
eq((tickets as any).userId, user.id),
eq((tickets as any).eventId, data.eventId)
const existingTicket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(
and(
eq((tickets as any).userId, user.id),
eq((tickets as any).eventId, data.eventId)
)
)
)
.get();
);
if (existingTicket && existingTicket.status !== 'cancelled') {
return c.json({ error: 'You have already booked this event' }, 400);
@@ -251,9 +258,11 @@ ticketsRouter.post('/', zValidator('json', createTicketSchema), async (c) => {
// Download ticket as PDF
ticketsRouter.get('/:id/pdf', async (c) => {
const id = c.req.param('id');
const user = await getAuthUser(c);
const user: any = await getAuthUser(c);
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -278,7 +287,9 @@ ticketsRouter.get('/:id/pdf', async (c) => {
}
// Get event
const event = await (db as any).select().from(events).where(eq((events as any).id, ticket.eventId)).get();
const event = await dbGet<any>(
(db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
);
if (!event) {
return c.json({ error: 'Event not found' }, 404);
@@ -316,17 +327,23 @@ ticketsRouter.get('/:id/pdf', async (c) => {
ticketsRouter.get('/:id', async (c) => {
const id = c.req.param('id');
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
}
// Get associated event
const event = await (db as any).select().from(events).where(eq((events as any).id, ticket.eventId)).get();
const event = await dbGet(
(db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
);
// Get payment
const payment = await (db as any).select().from(payments).where(eq((payments as any).ticketId, id)).get();
const payment = await dbGet(
(db as any).select().from(payments).where(eq((payments as any).ticketId, id))
);
return c.json({
ticket: {
@@ -342,7 +359,9 @@ ticketsRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff']), zValidat
const id = c.req.param('id');
const data = c.req.valid('json');
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -361,7 +380,9 @@ ticketsRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff']), zValidat
await (db as any).update(tickets).set(updates).where(eq((tickets as any).id, id));
}
const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const updated = await dbGet(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
return c.json({ ticket: updated });
});
@@ -376,19 +397,21 @@ ticketsRouter.post('/validate', requireAuth(['admin', 'organizer', 'staff']), as
}
// Try to find ticket by QR code or ID
let ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).qrCode, code))
.get();
let ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).qrCode, code))
);
// If not found by QR, try by ID
if (!ticket) {
ticket = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, code))
.get();
ticket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).id, code))
);
}
if (!ticket) {
@@ -409,11 +432,12 @@ ticketsRouter.post('/validate', requireAuth(['admin', 'organizer', 'staff']), as
}
// Get event details
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet<any>(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
// Determine validity status
let validityStatus = 'invalid';
@@ -433,11 +457,12 @@ ticketsRouter.post('/validate', requireAuth(['admin', 'organizer', 'staff']), as
// Get admin who checked in (if applicable)
let checkedInBy = null;
if (ticket.checkedInByAdminId) {
const admin = await (db as any)
.select()
.from(users)
.where(eq((users as any).id, ticket.checkedInByAdminId))
.get();
const admin = await dbGet<any>(
(db as any)
.select()
.from(users)
.where(eq((users as any).id, ticket.checkedInByAdminId))
);
checkedInBy = admin ? admin.name : null;
}
@@ -469,7 +494,9 @@ ticketsRouter.post('/:id/checkin', requireAuth(['admin', 'organizer', 'staff']),
const id = c.req.param('id');
const adminUser = (c as any).get('user');
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -494,10 +521,14 @@ ticketsRouter.post('/:id/checkin', requireAuth(['admin', 'organizer', 'staff']),
})
.where(eq((tickets as any).id, id));
const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const updated = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
// Get event for response
const event = await (db as any).select().from(events).where(eq((events as any).id, ticket.eventId)).get();
const event = await dbGet<any>(
(db as any).select().from(events).where(eq((events as any).id, ticket.eventId))
);
return c.json({
ticket: {
@@ -517,7 +548,9 @@ ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']
const id = c.req.param('id');
const user = (c as any).get('user');
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -551,11 +584,12 @@ ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']
.where(eq((payments as any).ticketId, id));
// Get payment for sending receipt
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, id))
);
// Send confirmation emails asynchronously (don't block the response)
Promise.all([
@@ -565,7 +599,9 @@ ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']
console.error('[Email] Failed to send confirmation emails:', err);
});
const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const updated = await dbGet(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
return c.json({ ticket: updated, message: 'Payment marked as received' });
});
@@ -575,18 +611,21 @@ ticketsRouter.post('/:id/mark-paid', requireAuth(['admin', 'organizer', 'staff']
ticketsRouter.post('/:id/mark-payment-sent', async (c) => {
const id = c.req.param('id');
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
}
// Get the payment
const payment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, id))
.get();
const payment = await dbGet<any>(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).ticketId, id))
);
if (!payment) {
return c.json({ error: 'Payment not found' }, 404);
@@ -632,11 +671,12 @@ ticketsRouter.post('/:id/mark-payment-sent', async (c) => {
.where(eq((payments as any).id, payment.id));
// Get updated payment
const updatedPayment = await (db as any)
.select()
.from(payments)
.where(eq((payments as any).id, payment.id))
.get();
const updatedPayment = await dbGet(
(db as any)
.select()
.from(payments)
.where(eq((payments as any).id, payment.id))
);
// TODO: Send notification to admin about pending payment approval
@@ -649,9 +689,11 @@ ticketsRouter.post('/:id/mark-payment-sent', async (c) => {
// Cancel ticket
ticketsRouter.post('/:id/cancel', async (c) => {
const id = c.req.param('id');
const user = await getAuthUser(c);
const user: any = await getAuthUser(c);
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -675,7 +717,9 @@ ticketsRouter.post('/:id/cancel', async (c) => {
ticketsRouter.post('/:id/remove-checkin', requireAuth(['admin', 'organizer', 'staff']), async (c) => {
const id = c.req.param('id');
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -690,7 +734,9 @@ ticketsRouter.post('/:id/remove-checkin', requireAuth(['admin', 'organizer', 'st
.set({ status: 'confirmed', checkinAt: null })
.where(eq((tickets as any).id, id));
const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const updated = await dbGet(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
return c.json({ ticket: updated, message: 'Check-in removed successfully' });
});
@@ -700,7 +746,9 @@ ticketsRouter.post('/:id/note', requireAuth(['admin', 'organizer', 'staff']), zV
const id = c.req.param('id');
const { note } = c.req.valid('json');
const ticket = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const ticket = await dbGet<any>(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
if (!ticket) {
return c.json({ error: 'Ticket not found' }, 404);
@@ -711,7 +759,9 @@ ticketsRouter.post('/:id/note', requireAuth(['admin', 'organizer', 'staff']), zV
.set({ adminNote: note || null })
.where(eq((tickets as any).id, id));
const updated = await (db as any).select().from(tickets).where(eq((tickets as any).id, id)).get();
const updated = await dbGet(
(db as any).select().from(tickets).where(eq((tickets as any).id, id))
);
return c.json({ ticket: updated, message: 'Note updated successfully' });
});
@@ -721,22 +771,25 @@ ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff'])
const data = c.req.valid('json');
// Get event
const event = await (db as any).select().from(events).where(eq((events as any).id, data.eventId)).get();
const event = await dbGet<any>(
(db as any).select().from(events).where(eq((events as any).id, data.eventId))
);
if (!event) {
return c.json({ error: 'Event not found' }, 404);
}
// Check capacity
const ticketCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, data.eventId),
sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
const ticketCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(tickets)
.where(
and(
eq((tickets as any).eventId, data.eventId),
sql`${(tickets as any).status} IN ('confirmed', 'checked_in')`
)
)
)
.get();
);
if ((ticketCount?.count || 0) >= event.capacity) {
return c.json({ error: 'Event is at capacity' }, 400);
@@ -750,7 +803,9 @@ ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff'])
: `door-${generateId()}@doorentry.local`;
// Find or create user
let user = await (db as any).select().from(users).where(eq((users as any).email, attendeeEmail)).get();
let user = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).email, attendeeEmail))
);
const adminFullName = data.lastName && data.lastName.trim()
? `${data.firstName} ${data.lastName}`.trim()
@@ -774,16 +829,17 @@ ticketsRouter.post('/admin/create', requireAuth(['admin', 'organizer', 'staff'])
// Check for existing active ticket for this user and event (only if real email provided)
if (data.email && data.email.trim() && !data.email.includes('@doorentry.local')) {
const existingTicket = await (db as any)
.select()
.from(tickets)
.where(
and(
eq((tickets as any).userId, user.id),
eq((tickets as any).eventId, data.eventId)
const existingTicket = await dbGet<any>(
(db as any)
.select()
.from(tickets)
.where(
and(
eq((tickets as any).userId, user.id),
eq((tickets as any).eventId, data.eventId)
)
)
)
.get();
);
if (existingTicket && existingTicket.status !== 'cancelled') {
return c.json({ error: 'This person already has a ticket for this event' }, 400);
@@ -869,7 +925,7 @@ ticketsRouter.get('/', requireAuth(['admin', 'organizer']), async (c) => {
query = query.where(and(...conditions));
}
const result = await query.all();
const result = await dbAll(query);
return c.json({ tickets: result });
});

View File

@@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, users, tickets, events, payments } from '../db/index.js';
import { db, dbGet, dbAll, users, tickets, events, payments } from '../db/index.js';
import { eq, desc, sql } from 'drizzle-orm';
import { requireAuth } from '../lib/auth.js';
import { getNow } from '../lib/utils.js';
@@ -40,7 +40,7 @@ usersRouter.get('/', requireAuth(['admin']), async (c) => {
query = query.where(eq((users as any).role, role));
}
const result = await query.orderBy(desc((users as any).createdAt)).all();
const result = await dbAll(query.orderBy(desc((users as any).createdAt)));
return c.json({ users: result });
});
@@ -55,19 +55,20 @@ usersRouter.get('/:id', requireAuth(['admin', 'organizer', 'staff', 'marketing',
return c.json({ error: 'Forbidden' }, 403);
}
const user = await (db as any)
.select({
id: (users as any).id,
email: (users as any).email,
name: (users as any).name,
phone: (users as any).phone,
role: (users as any).role,
languagePreference: (users as any).languagePreference,
createdAt: (users as any).createdAt,
})
.from(users)
.where(eq((users as any).id, id))
.get();
const user = await dbGet(
(db as any)
.select({
id: (users as any).id,
email: (users as any).email,
name: (users as any).name,
phone: (users as any).phone,
role: (users as any).role,
languagePreference: (users as any).languagePreference,
createdAt: (users as any).createdAt,
})
.from(users)
.where(eq((users as any).id, id))
);
if (!user) {
return c.json({ error: 'User not found' }, 404);
@@ -92,7 +93,9 @@ usersRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff', 'marketing',
delete data.role;
}
const existing = await (db as any).select().from(users).where(eq((users as any).id, id)).get();
const existing = await dbGet(
(db as any).select().from(users).where(eq((users as any).id, id))
);
if (!existing) {
return c.json({ error: 'User not found' }, 404);
}
@@ -102,18 +105,19 @@ usersRouter.put('/:id', requireAuth(['admin', 'organizer', 'staff', 'marketing',
.set({ ...data, updatedAt: getNow() })
.where(eq((users as any).id, id));
const updated = await (db as any)
.select({
id: (users as any).id,
email: (users as any).email,
name: (users as any).name,
phone: (users as any).phone,
role: (users as any).role,
languagePreference: (users as any).languagePreference,
})
.from(users)
.where(eq((users as any).id, id))
.get();
const updated = await dbGet(
(db as any)
.select({
id: (users as any).id,
email: (users as any).email,
name: (users as any).name,
phone: (users as any).phone,
role: (users as any).role,
languagePreference: (users as any).languagePreference,
})
.from(users)
.where(eq((users as any).id, id))
);
return c.json({ user: updated });
});
@@ -128,21 +132,23 @@ usersRouter.get('/:id/history', requireAuth(['admin', 'organizer', 'staff', 'mar
return c.json({ error: 'Forbidden' }, 403);
}
const userTickets = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, id))
.orderBy(desc((tickets as any).createdAt))
.all();
const userTickets = await dbAll<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, id))
.orderBy(desc((tickets as any).createdAt))
);
// Get event details for each ticket
const history = await Promise.all(
userTickets.map(async (ticket: any) => {
const event = await (db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
.get();
const event = await dbGet(
(db as any)
.select()
.from(events)
.where(eq((events as any).id, ticket.eventId))
);
return {
...ticket,
@@ -164,7 +170,9 @@ usersRouter.delete('/:id', requireAuth(['admin']), async (c) => {
return c.json({ error: 'Cannot delete your own account' }, 400);
}
const existing = await (db as any).select().from(users).where(eq((users as any).id, id)).get();
const existing = await dbGet<any>(
(db as any).select().from(users).where(eq((users as any).id, id))
);
if (!existing) {
return c.json({ error: 'User not found' }, 404);
}
@@ -176,11 +184,12 @@ usersRouter.delete('/:id', requireAuth(['admin']), async (c) => {
try {
// Get all tickets for this user
const userTickets = await (db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, id))
.all();
const userTickets = await dbAll<any>(
(db as any)
.select()
.from(tickets)
.where(eq((tickets as any).userId, id))
);
// Delete payments associated with user's tickets
for (const ticket of userTickets) {
@@ -202,16 +211,18 @@ usersRouter.delete('/:id', requireAuth(['admin']), async (c) => {
// Get user statistics (admin)
usersRouter.get('/stats/overview', requireAuth(['admin']), async (c) => {
const totalUsers = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(users)
.get();
const totalUsers = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(users)
);
const adminCount = await (db as any)
.select({ count: sql<number>`count(*)` })
.from(users)
.where(eq((users as any).role, 'admin'))
.get();
const adminCount = await dbGet<any>(
(db as any)
.select({ count: sql<number>`count(*)` })
.from(users)
.where(eq((users as any).role, 'admin'))
);
return c.json({
stats: {