← back to IWasCute
db/001_schema.sql
257 lines
-- Iwascute.com — Core Schema
-- Dual-rights clearance marketplace for childhood photos
-- Extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================================
-- USERS
-- ============================================================
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT UNIQUE NOT NULL,
password_hash TEXT, -- NULL for OAuth-only users
display_name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'licensor' CHECK (role IN ('licensor', 'licensee', 'admin')),
is_verified BOOLEAN DEFAULT FALSE,
is_adult_confirmed BOOLEAN DEFAULT FALSE,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Admin user
INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
VALUES (
'admin@iwascute.com',
crypt('DWSecure2024!', gen_salt('bf')),
'Admin',
'admin',
TRUE,
TRUE
);
-- ============================================================
-- UPLOADS (a submission = batch of photos + rights info)
-- ============================================================
CREATE TABLE uploads (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'in_review', 'approved', 'rejected', 'flagged', 'dmca_removed')),
-- Copyright info (step 2)
copyright_relationship TEXT CHECK (copyright_relationship IN ('parent', 'family', 'professional', 'unknown')),
copyright_photographer TEXT,
copyright_year INTEGER,
copyright_permission TEXT CHECK (copyright_permission IN ('yes', 'no', 'deceased')),
copyright_estate_rights BOOLEAN DEFAULT FALSE,
-- Likeness release (step 3)
likeness_is_subject BOOLEAN DEFAULT FALSE,
likeness_is_adult BOOLEAN DEFAULT FALSE,
likeness_editorial BOOLEAN DEFAULT FALSE,
likeness_commercial BOOLEAN DEFAULT FALSE,
likeness_ai_training BOOLEAN DEFAULT FALSE,
likeness_signature TEXT,
likeness_signed_at TIMESTAMPTZ,
-- Review
reviewed_by UUID REFERENCES users(id),
reviewed_at TIMESTAMPTZ,
review_notes TEXT,
rejection_reason TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- PHOTOS (individual images within an upload)
-- ============================================================
CREATE TABLE photos (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
upload_id UUID NOT NULL REFERENCES uploads(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- File info
filename TEXT NOT NULL,
file_size INTEGER,
mime_type TEXT,
storage_path TEXT, -- future: S3/R2 path
thumbnail_path TEXT,
-- EXIF metadata
exif_date_taken TIMESTAMPTZ,
exif_year INTEGER,
exif_latitude DOUBLE PRECISION,
exif_longitude DOUBLE PRECISION,
exif_camera_make TEXT,
exif_camera_model TEXT,
exif_width INTEGER,
exif_height INTEGER,
-- Clearance status (inherits from upload, can be overridden per-photo)
clearance_level TEXT DEFAULT 'none'
CHECK (clearance_level IN ('none', 'editorial', 'commercial', 'ai_training')),
-- Licensing
view_count INTEGER DEFAULT 0,
license_count INTEGER DEFAULT 0,
total_earnings NUMERIC(10,2) DEFAULT 0,
-- Directory grouping
directory_name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- DMCA CLAIMS
-- ============================================================
CREATE TABLE dmca_claims (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
upload_id UUID REFERENCES uploads(id),
photo_id UUID REFERENCES photos(id),
claimant_name TEXT NOT NULL,
claimant_email TEXT,
reason TEXT NOT NULL,
evidence_url TEXT,
status TEXT NOT NULL DEFAULT 'open'
CHECK (status IN ('open', 'investigating', 'taken_down', 'counter_noticed', 'resolved', 'dismissed')),
-- Resolution
resolved_by UUID REFERENCES users(id),
resolved_at TIMESTAMPTZ,
resolution_notes TEXT,
filed_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- AUDIT LOG
-- ============================================================
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id),
action TEXT NOT NULL, -- 'upload.approve', 'upload.reject', 'dmca.takedown', etc
target_type TEXT, -- 'upload', 'photo', 'dmca_claim', 'user'
target_id UUID,
details JSONB,
ip_address INET,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- SESSIONS (simple cookie auth)
-- ============================================================
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token TEXT UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(32), 'hex'),
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- INDEXES
-- ============================================================
CREATE INDEX idx_uploads_user ON uploads(user_id);
CREATE INDEX idx_uploads_status ON uploads(status);
CREATE INDEX idx_photos_upload ON photos(upload_id);
CREATE INDEX idx_photos_user ON photos(user_id);
CREATE INDEX idx_dmca_status ON dmca_claims(status);
CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
CREATE INDEX idx_audit_user ON audit_log(user_id);
CREATE INDEX idx_audit_action ON audit_log(action);
-- ============================================================
-- SEED DATA (demo uploads for admin testing)
-- ============================================================
DO $$
DECLARE
u1 UUID; u2 UUID; u3 UUID; u4 UUID; u5 UUID; u6 UUID;
up1 UUID; up2 UUID; up3 UUID; up4 UUID; up5 UUID; up6 UUID;
BEGIN
-- Create demo users
INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
VALUES ('sarah.m@email.com', crypt('demo123', gen_salt('bf')), 'Sarah Martinez', 'licensor', TRUE, TRUE) RETURNING id INTO u1;
INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
VALUES ('james.k@email.com', crypt('demo123', gen_salt('bf')), 'James Kim', 'licensor', TRUE, TRUE) RETURNING id INTO u2;
INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
VALUES ('maria.l@email.com', crypt('demo123', gen_salt('bf')), 'Maria Lopez', 'licensor', TRUE, TRUE) RETURNING id INTO u3;
INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
VALUES ('david.r@email.com', crypt('demo123', gen_salt('bf')), 'David Reynolds', 'licensor', TRUE, TRUE) RETURNING id INTO u4;
INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
VALUES ('emma.t@email.com', crypt('demo123', gen_salt('bf')), 'Emma Thompson', 'licensor', TRUE, TRUE) RETURNING id INTO u5;
INSERT INTO users (email, password_hash, display_name, role, is_verified, is_adult_confirmed)
VALUES ('alex.w@email.com', crypt('demo123', gen_salt('bf')), 'Alex Wilson', 'licensor', TRUE, TRUE) RETURNING id INTO u6;
-- Create uploads (pending review)
INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_signature, likeness_signed_at)
VALUES (u1, 'pending', 'parent', 'yes', TRUE, TRUE, TRUE, TRUE, 'Sarah Martinez', NOW() - INTERVAL '2 hours') RETURNING id INTO up1;
INSERT INTO uploads (user_id, status, copyright_relationship, copyright_photographer, copyright_permission, copyright_estate_rights, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_signature, likeness_signed_at)
VALUES (u2, 'pending', 'professional', 'Jane Doe Photography', 'deceased', TRUE, TRUE, TRUE, TRUE, 'James Kim', NOW() - INTERVAL '5 hours') RETURNING id INTO up2;
INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_ai_training, likeness_signature, likeness_signed_at)
VALUES (u3, 'pending', 'family', 'yes', TRUE, TRUE, TRUE, TRUE, TRUE, 'Maria Lopez', NOW() - INTERVAL '8 hours') RETURNING id INTO up3;
INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_signature, likeness_signed_at)
VALUES (u4, 'flagged', 'unknown', 'no', TRUE, TRUE, TRUE, 'David Reynolds', NOW() - INTERVAL '12 hours') RETURNING id INTO up4;
INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_signature, likeness_signed_at)
VALUES (u5, 'pending', 'parent', 'yes', TRUE, TRUE, TRUE, TRUE, 'Emma Thompson', NOW() - INTERVAL '1 day') RETURNING id INTO up5;
INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, copyright_estate_rights, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_ai_training, likeness_signature, likeness_signed_at)
VALUES (u6, 'pending', 'parent', 'deceased', TRUE, TRUE, TRUE, TRUE, TRUE, TRUE, 'Alex Wilson', NOW() - INTERVAL '1 day') RETURNING id INTO up6;
-- Add photos to uploads
INSERT INTO photos (upload_id, user_id, filename, file_size, mime_type, exif_year) VALUES
(up1, u1, 'summer_1992.jpg', 2400000, 'image/jpeg', 1992),
(up1, u1, 'birthday_party.jpg', 1800000, 'image/jpeg', 1993),
(up1, u1, 'first_steps.jpg', 3100000, 'image/jpeg', 1991),
(up1, u1, 'beach_day.jpg', 2200000, 'image/jpeg', 1994),
(up2, u2, 'school_portrait.jpg', 4500000, 'image/jpeg', 1988),
(up3, u3, 'christmas_1995.jpg', 1900000, 'image/jpeg', 1995),
(up3, u3, 'halloween.jpg', 2100000, 'image/jpeg', 1996),
(up3, u3, 'easter_egg_hunt.jpg', 1700000, 'image/jpeg', 1995),
(up3, u3, 'playground.jpg', 2800000, 'image/jpeg', 1997),
(up3, u3, 'first_bike.jpg', 3200000, 'image/jpeg', 1996),
(up3, u3, 'family_vacation.jpg', 2500000, 'image/jpeg', 1998),
(up3, u3, 'grandma_house.jpg', 1600000, 'image/jpeg', 1994),
(up3, u3, 'snow_day.jpg', 2900000, 'image/jpeg', 1997),
(up4, u4, 'old_photo_1.jpg', 1200000, 'image/jpeg', NULL),
(up4, u4, 'old_photo_2.jpg', 1500000, 'image/jpeg', NULL),
(up5, u5, 'toddler_pic.jpg', 2600000, 'image/jpeg', 2000),
(up5, u5, 'kindergarten.jpg', 2100000, 'image/jpeg', 2002),
(up5, u5, 'dance_recital.jpg', 3400000, 'image/jpeg', 2003),
(up6, u6, 'baby_photo_1.jpg', 1800000, 'image/jpeg', 1985),
(up6, u6, 'baby_photo_2.jpg', 2200000, 'image/jpeg', 1985),
(up6, u6, 'baby_photo_3.jpg', 1900000, 'image/jpeg', 1986),
(up6, u6, 'toddler_1.jpg', 2700000, 'image/jpeg', 1987),
(up6, u6, 'toddler_2.jpg', 2400000, 'image/jpeg', 1987),
(up6, u6, 'preschool.jpg', 3000000, 'image/jpeg', 1988);
-- DMCA claims
INSERT INTO dmca_claims (upload_id, claimant_name, claimant_email, reason, status, filed_at) VALUES
(up1, 'John Smith Photography', 'john@smithphoto.com', 'Copyright infringement — claims to be the original photographer of these images', 'open', NOW() - INTERVAL '3 days'),
(up3, 'Getty Images Legal', 'legal@gettyimages.com', 'Stock photo mistakenly uploaded as personal childhood photo', 'resolved', NOW() - INTERVAL '7 days'),
(up5, 'Anonymous', NULL, 'Subject requests removal — disputes likeness consent was given', 'open', NOW() - INTERVAL '2 days');
-- Some approved uploads (historical)
FOR i IN 1..20 LOOP
INSERT INTO uploads (user_id, status, copyright_relationship, copyright_permission, likeness_is_subject, likeness_is_adult, likeness_editorial, likeness_commercial, likeness_signature, likeness_signed_at, reviewed_at)
VALUES (
(ARRAY[u1, u2, u3, u5, u6])[1 + (i % 5)],
'approved',
(ARRAY['parent', 'family', 'professional'])[1 + (i % 3)],
'yes', TRUE, TRUE, TRUE, TRUE,
'Demo User ' || i,
NOW() - (i || ' days')::INTERVAL,
NOW() - ((i - 1) || ' days')::INTERVAL
);
END LOOP;
END $$;