feat: Foundation — auth system, 9 migrations, React frontend
Backend: - Express server with JWT httpOnly cookie auth - POST /api/auth/register, /api/auth/login, /api/auth/logout, GET /api/auth/me - bcrypt 12 rounds, generic 401 errors (no email/password field disclosure) - Auth middleware protects all /api/* routes except register/login - pg Pool database connection Frontend (React + Vite + TailwindCSS + shadcn/ui): - AuthContext with session restore on page load via /api/auth/me - ProtectedRoute redirects unauthenticated users to /login - LoginPage, RegisterPage — Hebrew RTL layout (dir=rtl), inline validation - DashboardPage placeholder - shadcn/ui components: Button, Input, Label, Card Database: - 9 migrations (001-009): extensions, users, events, vendors, guests, bookings, invitations, vendor_ratings, organizer_preferences - pg_trgm for fuzzy Hebrew search, GIN indexes on style_tags - Phase 2+3 fields included: source, payment_status, contract_value, vendor ratings 6-dimension, organizer preferences - Idempotent migration runner with schema_migrations tracking table Infrastructure: - Dockerfile (multi-stage: build React → production node:20-alpine) - docker-compose.yml with PostgreSQL healthcheck, expose not ports - Migrations run automatically on container start Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
12
migrations/001_create_extensions.sql
Normal file
12
migrations/001_create_extensions.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- Migration 001: Enable required PostgreSQL extensions
|
||||
-- UP
|
||||
BEGIN;
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- required for Phase 2 fuzzy Hebrew name search
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP EXTENSION IF EXISTS "pg_trgm";
|
||||
-- DROP EXTENSION IF EXISTS "pgcrypto";
|
||||
-- COMMIT;
|
||||
25
migrations/002_create_users.sql
Normal file
25
migrations/002_create_users.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- Migration 002: Create users table
|
||||
-- UP
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE user_role AS ENUM ('organizer', 'vendor', 'admin');
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
role user_role NOT NULL DEFAULT 'organizer',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS users;
|
||||
-- DROP TYPE IF EXISTS user_role;
|
||||
-- COMMIT;
|
||||
43
migrations/003_create_events.sql
Normal file
43
migrations/003_create_events.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- Migration 003: Create events table
|
||||
-- UP
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE event_status AS ENUM ('draft', 'published', 'cancelled', 'completed');
|
||||
CREATE TYPE kashrut_level AS ENUM ('none', 'regular', 'mehadrin', 'chalav_yisrael');
|
||||
CREATE TYPE event_language AS ENUM ('hebrew', 'arabic', 'english');
|
||||
|
||||
CREATE TABLE events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
organizer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
event_date TIMESTAMPTZ,
|
||||
venue_name VARCHAR(255),
|
||||
venue_address TEXT,
|
||||
max_guests INTEGER,
|
||||
venue_capacity INTEGER, -- fire-safety hard limit
|
||||
max_plus_ones_buffer INTEGER NOT NULL DEFAULT 30, -- % buffer for walk-ins
|
||||
status event_status NOT NULL DEFAULT 'draft',
|
||||
kashrut_level kashrut_level NOT NULL DEFAULT 'none',
|
||||
noise_curfew_time TIME NOT NULL DEFAULT '23:00', -- Israeli law default
|
||||
language_pref event_language NOT NULL DEFAULT 'hebrew',
|
||||
budget DECIMAL(12, 2),
|
||||
retention_policy_days INTEGER NOT NULL DEFAULT 365, -- Israeli Privacy Law 2023
|
||||
deleted_at TIMESTAMPTZ, -- soft delete for organizer use
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_events_organizer_id ON events(organizer_id);
|
||||
CREATE INDEX idx_events_status ON events(status);
|
||||
CREATE INDEX idx_events_event_date ON events(event_date);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS events;
|
||||
-- DROP TYPE IF EXISTS event_language;
|
||||
-- DROP TYPE IF EXISTS kashrut_level;
|
||||
-- DROP TYPE IF EXISTS event_status;
|
||||
-- COMMIT;
|
||||
50
migrations/004_create_vendors.sql
Normal file
50
migrations/004_create_vendors.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
-- Migration 004: Create vendors table
|
||||
-- UP
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE vendor_category AS ENUM (
|
||||
'catering', 'photography', 'videographer', 'music', 'decoration',
|
||||
'venue', 'officiant', 'staffing', 'transportation', 'printing',
|
||||
'entertainment', 'other'
|
||||
);
|
||||
|
||||
CREATE TABLE vendors (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
business_name VARCHAR(255) NOT NULL,
|
||||
category vendor_category NOT NULL,
|
||||
description TEXT,
|
||||
base_price DECIMAL(12, 2),
|
||||
city VARCHAR(100),
|
||||
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
-- Israeli compliance & certification fields
|
||||
kashrut_cert_number VARCHAR(100),
|
||||
kashrut_issuing_authority VARCHAR(255),
|
||||
business_license_number VARCHAR(100),
|
||||
license_expiry_date DATE, -- alert when within 30 days of expiry
|
||||
insurance_ref VARCHAR(255),
|
||||
-- Phase 3: AI recommendation fields
|
||||
geographic_area VARCHAR(255), -- broader area (e.g. "North", "Tel Aviv District")
|
||||
price_range_min DECIMAL(12, 2), -- NIS
|
||||
price_range_max DECIMAL(12, 2), -- NIS
|
||||
capacity_min INTEGER,
|
||||
capacity_max INTEGER,
|
||||
style_tags TEXT[], -- e.g. {"rustic","modern","religious"}
|
||||
deleted_at TIMESTAMPTZ, -- soft delete
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vendors_user_id ON vendors(user_id);
|
||||
CREATE INDEX idx_vendors_category ON vendors(category);
|
||||
CREATE INDEX idx_vendors_city ON vendors(city);
|
||||
CREATE INDEX idx_vendors_geographic ON vendors(geographic_area);
|
||||
CREATE INDEX idx_vendors_style_tags ON vendors USING GIN(style_tags);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS vendors;
|
||||
-- DROP TYPE IF EXISTS vendor_category;
|
||||
-- COMMIT;
|
||||
56
migrations/005_create_guests.sql
Normal file
56
migrations/005_create_guests.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- Migration 005: Create guests table
|
||||
-- UP
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE rsvp_status AS ENUM ('pending', 'confirmed', 'declined');
|
||||
CREATE TYPE relationship_group AS ENUM ('family_bride', 'family_groom', 'friends', 'work', 'community', 'other');
|
||||
CREATE TYPE dietary_preference AS ENUM ('none', 'vegetarian', 'vegan', 'kosher_regular', 'kosher_mehadrin');
|
||||
CREATE TYPE guest_source AS ENUM ('registered', 'walkin'); -- Phase 2: analytics
|
||||
|
||||
CREATE TABLE guests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
-- Name (Hebrew required; Latin transliteration for non-Hebrew speakers)
|
||||
name_hebrew VARCHAR(255) NOT NULL,
|
||||
name_transliteration VARCHAR(255),
|
||||
-- Contact (Israeli E.164 phone format: +972XXXXXXXXX)
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(20),
|
||||
-- RSVP
|
||||
rsvp_status rsvp_status NOT NULL DEFAULT 'pending',
|
||||
-- Seating
|
||||
table_number INTEGER,
|
||||
seat_number VARCHAR(10),
|
||||
-- Social grouping
|
||||
relationship_group relationship_group,
|
||||
plus_one_of UUID REFERENCES guests(id) ON DELETE SET NULL, -- self-ref FK
|
||||
plus_one_allowance INTEGER NOT NULL DEFAULT 0,
|
||||
-- Preferences
|
||||
dietary_preference dietary_preference NOT NULL DEFAULT 'none',
|
||||
dietary_notes TEXT, -- free-text override/additions
|
||||
accessibility_needs TEXT,
|
||||
-- Phase 2: Day-of check-in
|
||||
source guest_source NOT NULL DEFAULT 'registered', -- analytics (walk-ins vs pre-registered)
|
||||
-- Israeli Privacy Law 2023 compliance
|
||||
privacy_accepted_at TIMESTAMPTZ,
|
||||
-- NO deleted_at: guests support hard delete only (data subject right per Israeli Privacy Law)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_guests_event_id ON guests(event_id);
|
||||
CREATE INDEX idx_guests_rsvp_status ON guests(rsvp_status);
|
||||
CREATE INDEX idx_guests_plus_one_of ON guests(plus_one_of);
|
||||
-- pg_trgm GIN index for Phase 2 fuzzy Hebrew name search (requires pg_trgm from migration 001)
|
||||
CREATE INDEX idx_guests_name_trgm ON guests USING GIN(name_hebrew gin_trgm_ops);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS guests;
|
||||
-- DROP TYPE IF EXISTS guest_source;
|
||||
-- DROP TYPE IF EXISTS dietary_preference;
|
||||
-- DROP TYPE IF EXISTS relationship_group;
|
||||
-- DROP TYPE IF EXISTS rsvp_status;
|
||||
-- COMMIT;
|
||||
35
migrations/006_create_bookings.sql
Normal file
35
migrations/006_create_bookings.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- Migration 006: Create bookings table
|
||||
-- UP
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled');
|
||||
CREATE TYPE payment_status AS ENUM ('unpaid', 'deposit_paid', 'fully_paid'); -- Phase 3: AI/financial
|
||||
|
||||
CREATE TABLE bookings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||||
status booking_status NOT NULL DEFAULT 'pending',
|
||||
agreed_price DECIMAL(12, 2),
|
||||
notes TEXT,
|
||||
-- Phase 3: AI recommendation & financial tracking
|
||||
contract_value DECIMAL(12, 2), -- actual signed contract value in NIS
|
||||
payment_status payment_status NOT NULL DEFAULT 'unpaid',
|
||||
deleted_at TIMESTAMPTZ, -- soft delete
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_bookings_event_id ON bookings(event_id);
|
||||
CREATE INDEX idx_bookings_vendor_id ON bookings(vendor_id);
|
||||
CREATE INDEX idx_bookings_status ON bookings(status);
|
||||
CREATE INDEX idx_bookings_payment_status ON bookings(payment_status);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS bookings;
|
||||
-- DROP TYPE IF EXISTS payment_status;
|
||||
-- DROP TYPE IF EXISTS booking_status;
|
||||
-- COMMIT;
|
||||
31
migrations/007_create_invitations.sql
Normal file
31
migrations/007_create_invitations.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
-- Migration 007: Create invitations table
|
||||
-- UP
|
||||
BEGIN;
|
||||
|
||||
CREATE TYPE invitation_channel AS ENUM ('sms', 'whatsapp', 'email');
|
||||
|
||||
CREATE TABLE invitations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
guest_id UUID NOT NULL REFERENCES guests(id) ON DELETE CASCADE,
|
||||
token VARCHAR(128) UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(64), 'hex'),
|
||||
channel invitation_channel NOT NULL DEFAULT 'whatsapp',
|
||||
-- MVP: wa.me deep-link (no Twilio/API required)
|
||||
-- Format: https://wa.me/+972XXXXXXXXX?text=ENCODED_MESSAGE
|
||||
whatsapp_link TEXT, -- pre-generated deep-link for organizer to click
|
||||
sent_at TIMESTAMPTZ, -- when organizer clicked Send
|
||||
opened_at TIMESTAMPTZ, -- when guest opened the RSVP link
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_invitations_event_id ON invitations(event_id);
|
||||
CREATE INDEX idx_invitations_guest_id ON invitations(guest_id);
|
||||
CREATE INDEX idx_invitations_token ON invitations(token);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS invitations;
|
||||
-- DROP TYPE IF EXISTS invitation_channel;
|
||||
-- COMMIT;
|
||||
35
migrations/008_create_vendor_ratings.sql
Normal file
35
migrations/008_create_vendor_ratings.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- Migration 008: Create vendor_ratings table (Phase 3: AI recommendation engine)
|
||||
-- UP
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE vendor_ratings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||||
organizer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
-- 6-dimension rating system (1-5 scale)
|
||||
quality_score SMALLINT NOT NULL CHECK (quality_score BETWEEN 1 AND 5),
|
||||
professionalism_score SMALLINT NOT NULL CHECK (professionalism_score BETWEEN 1 AND 5),
|
||||
flexibility_score SMALLINT NOT NULL CHECK (flexibility_score BETWEEN 1 AND 5),
|
||||
value_score SMALLINT NOT NULL CHECK (value_score BETWEEN 1 AND 5),
|
||||
-- Boolean recommendation signals
|
||||
would_use_again BOOLEAN NOT NULL,
|
||||
would_recommend BOOLEAN NOT NULL,
|
||||
-- Optional review text
|
||||
review_text TEXT,
|
||||
rated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
-- One rating per (event, vendor, organizer) tuple
|
||||
CONSTRAINT uq_vendor_rating UNIQUE (event_id, vendor_id, organizer_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_vendor_ratings_vendor_id ON vendor_ratings(vendor_id);
|
||||
CREATE INDEX idx_vendor_ratings_organizer_id ON vendor_ratings(organizer_id);
|
||||
CREATE INDEX idx_vendor_ratings_event_id ON vendor_ratings(event_id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS vendor_ratings;
|
||||
-- COMMIT;
|
||||
30
migrations/009_create_organizer_preferences.sql
Normal file
30
migrations/009_create_organizer_preferences.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- Migration 009: Create organizer_preferences table (Phase 3: AI recommendation engine)
|
||||
-- UP
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE organizer_preferences (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
-- Style preferences (matches vendors.style_tags for AI matching)
|
||||
style_tags TEXT[], -- e.g. {"rustic","modern","religious"}
|
||||
-- Typical event scale
|
||||
typical_guest_count_min INTEGER,
|
||||
typical_guest_count_max INTEGER,
|
||||
-- Typical budget range in NIS
|
||||
typical_budget_min DECIMAL(12, 2),
|
||||
typical_budget_max DECIMAL(12, 2),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
-- One preference record per user
|
||||
CONSTRAINT uq_organizer_preferences_user UNIQUE (user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_organizer_prefs_user_id ON organizer_preferences(user_id);
|
||||
CREATE INDEX idx_organizer_prefs_style_tags ON organizer_preferences USING GIN(style_tags);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN
|
||||
-- BEGIN;
|
||||
-- DROP TABLE IF EXISTS organizer_preferences;
|
||||
-- COMMIT;
|
||||
Reference in New Issue
Block a user