246 lines
9.8 KiB
PL/PgSQL
246 lines
9.8 KiB
PL/PgSQL
-- ExposedGays Supabase Schema
|
|
-- Run in Supabase SQL Editor or via CLI: supabase db push
|
|
|
|
-- Enable PostGIS for map geolocation
|
|
CREATE EXTENSION IF NOT EXISTS postgis;
|
|
|
|
-- ============================================================
|
|
-- PROFILES
|
|
-- ============================================================
|
|
CREATE TABLE public.profiles (
|
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
username TEXT UNIQUE,
|
|
display_name TEXT,
|
|
age INTEGER CHECK (age >= 18 AND age <= 120),
|
|
position TEXT CHECK (position IN ('top', 'bottom', 'vers', 'side')),
|
|
bio TEXT,
|
|
avatar_url TEXT,
|
|
status TEXT NOT NULL DEFAULT 'offline'
|
|
CHECK (status IN ('online', 'looking', 'hosting', 'traveling', 'offline')),
|
|
is_anonymous BOOLEAN NOT NULL DEFAULT true,
|
|
kinks TEXT[] DEFAULT '{}',
|
|
sti_status TEXT,
|
|
on_prep BOOLEAN DEFAULT false,
|
|
last_active TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
location GEOGRAPHY(POINT, 4326),
|
|
city TEXT,
|
|
vanilla_mode BOOLEAN DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_profiles_location ON public.profiles USING GIST (location);
|
|
CREATE INDEX idx_profiles_status ON public.profiles (status);
|
|
CREATE INDEX idx_profiles_city ON public.profiles (city);
|
|
CREATE INDEX idx_profiles_last_active ON public.profiles (last_active DESC);
|
|
|
|
-- ============================================================
|
|
-- CRUISING SPOTS
|
|
-- ============================================================
|
|
CREATE TABLE public.cruising_spots (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
category TEXT NOT NULL
|
|
CHECK (category IN ('park', 'beach', 'gym', 'restroom', 'bookstore', 'club', 'other')),
|
|
location GEOGRAPHY(POINT, 4326) NOT NULL,
|
|
city TEXT NOT NULL,
|
|
rating NUMERIC(2,1) DEFAULT 0,
|
|
active_count INTEGER DEFAULT 0,
|
|
is_popular BOOLEAN DEFAULT false,
|
|
created_by UUID REFERENCES public.profiles(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_spots_location ON public.cruising_spots USING GIST (location);
|
|
CREATE INDEX idx_spots_city ON public.cruising_spots (city);
|
|
CREATE INDEX idx_spots_category ON public.cruising_spots (category);
|
|
|
|
-- ============================================================
|
|
-- CHAT ROOMS
|
|
-- ============================================================
|
|
CREATE TABLE public.chat_rooms (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL CHECK (type IN ('city', 'spot', 'group', 'dm')),
|
|
city TEXT,
|
|
spot_id UUID REFERENCES public.cruising_spots(id) ON DELETE CASCADE,
|
|
created_by UUID REFERENCES public.profiles(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_rooms_type ON public.chat_rooms (type);
|
|
CREATE INDEX idx_rooms_city ON public.chat_rooms (city);
|
|
|
|
-- Room membership
|
|
CREATE TABLE public.chat_room_members (
|
|
room_id UUID REFERENCES public.chat_rooms(id) ON DELETE CASCADE,
|
|
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (room_id, user_id)
|
|
);
|
|
|
|
-- ============================================================
|
|
-- CHAT MESSAGES
|
|
-- ============================================================
|
|
CREATE TABLE public.chat_messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
room_id UUID NOT NULL REFERENCES public.chat_rooms(id) ON DELETE CASCADE,
|
|
sender_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
content TEXT NOT NULL,
|
|
image_url TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_messages_room ON public.chat_messages (room_id, created_at DESC);
|
|
CREATE INDEX idx_messages_sender ON public.chat_messages (sender_id);
|
|
|
|
-- Enable Realtime
|
|
ALTER PUBLICATION supabase_realtime ADD TABLE public.chat_messages;
|
|
ALTER PUBLICATION supabase_realtime ADD TABLE public.profiles;
|
|
|
|
-- ============================================================
|
|
-- PHOTO GALLERY
|
|
-- ============================================================
|
|
CREATE TABLE public.gallery_photos (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
url TEXT NOT NULL,
|
|
is_explicit BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_gallery_user ON public.gallery_photos (user_id);
|
|
|
|
-- ============================================================
|
|
-- SHOP PRODUCTS (optional DB-backed; static JSON also works)
|
|
-- ============================================================
|
|
CREATE TABLE public.shop_products (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
slug TEXT UNIQUE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
price NUMERIC(10,2) NOT NULL DEFAULT 0,
|
|
category TEXT NOT NULL,
|
|
image_url TEXT,
|
|
affiliate_url TEXT NOT NULL,
|
|
affiliate_label TEXT NOT NULL DEFAULT 'Buy Now',
|
|
kinks TEXT[] DEFAULT '{}',
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- ============================================================
|
|
-- HELPER: Update location from lat/lng
|
|
-- ============================================================
|
|
CREATE OR REPLACE FUNCTION public.update_profile_location(
|
|
p_user_id UUID,
|
|
p_lat DOUBLE PRECISION,
|
|
p_lng DOUBLE PRECISION
|
|
) RETURNS VOID AS $$
|
|
BEGIN
|
|
UPDATE public.profiles
|
|
SET location = ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
|
|
last_active = now(),
|
|
updated_at = now()
|
|
WHERE id = p_user_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
-- Auto-create profile on signup
|
|
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
INSERT INTO public.profiles (id, username, is_anonymous)
|
|
VALUES (NEW.id, NEW.email, true);
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
|
|
CREATE TRIGGER on_auth_user_created
|
|
AFTER INSERT ON auth.users
|
|
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
|
|
|
-- ============================================================
|
|
-- ROW LEVEL SECURITY
|
|
-- ============================================================
|
|
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.cruising_spots ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.chat_rooms ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.chat_room_members ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.chat_messages ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.gallery_photos ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE public.shop_products ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Profiles: anyone can read, owners can update
|
|
CREATE POLICY "Profiles are viewable by everyone"
|
|
ON public.profiles FOR SELECT USING (true);
|
|
|
|
CREATE POLICY "Users can update own profile"
|
|
ON public.profiles FOR UPDATE USING (auth.uid() = id);
|
|
|
|
CREATE POLICY "Users can insert own profile"
|
|
ON public.profiles FOR INSERT WITH CHECK (auth.uid() = id);
|
|
|
|
-- Spots: public read, authenticated create
|
|
CREATE POLICY "Spots are viewable by everyone"
|
|
ON public.cruising_spots FOR SELECT USING (true);
|
|
|
|
CREATE POLICY "Authenticated users can create spots"
|
|
ON public.cruising_spots FOR INSERT
|
|
WITH CHECK (auth.role() = 'authenticated');
|
|
|
|
-- Chat rooms: members can read
|
|
CREATE POLICY "Chat rooms viewable by authenticated users"
|
|
ON public.chat_rooms FOR SELECT
|
|
USING (auth.role() = 'authenticated');
|
|
|
|
CREATE POLICY "Authenticated users can create rooms"
|
|
ON public.chat_rooms FOR INSERT
|
|
WITH CHECK (auth.role() = 'authenticated');
|
|
|
|
-- Chat messages: room members can read/write
|
|
CREATE POLICY "Messages viewable by authenticated users"
|
|
ON public.chat_messages FOR SELECT
|
|
USING (auth.role() = 'authenticated');
|
|
|
|
CREATE POLICY "Authenticated users can send messages"
|
|
ON public.chat_messages FOR INSERT
|
|
WITH CHECK (auth.uid() = sender_id);
|
|
|
|
-- Gallery: public read for non-anonymous, owner write
|
|
CREATE POLICY "Gallery photos viewable by everyone"
|
|
ON public.gallery_photos FOR SELECT USING (true);
|
|
|
|
CREATE POLICY "Users can manage own gallery"
|
|
ON public.gallery_photos FOR ALL USING (auth.uid() = user_id);
|
|
|
|
-- Shop products: public read
|
|
CREATE POLICY "Shop products are public"
|
|
ON public.shop_products FOR SELECT USING (is_active = true);
|
|
|
|
-- ============================================================
|
|
-- SEED: Fort Lauderdale city chat room
|
|
-- ============================================================
|
|
INSERT INTO public.chat_rooms (name, type, city)
|
|
VALUES ('Fort Lauderdale Chat', 'city', 'Fort Lauderdale');
|
|
|
|
-- SEED: Sample cruising spots
|
|
INSERT INTO public.cruising_spots (name, description, category, location, city, rating, active_count, is_popular)
|
|
VALUES
|
|
('Hugh Taylor Birch State Park', 'Popular trails behind the nature center.', 'park',
|
|
ST_SetSRID(ST_MakePoint(-80.1048, 26.1452), 4326)::geography, 'Fort Lauderdale', 4.2, 8, true),
|
|
('Sebastian Street Beach', 'Gay beach section. Day and night action.', 'beach',
|
|
ST_SetSRID(ST_MakePoint(-80.1042, 26.1175), 4326)::geography, 'Fort Lauderdale', 4.5, 12, true),
|
|
('Triple Crown Bookstore', 'Adult bookstore with viewing booths.', 'bookstore',
|
|
ST_SetSRID(ST_MakePoint(-80.1534, 26.1912), 4326)::geography, 'Oakland Park', 4.0, 6, true);
|
|
|
|
-- SEED: Shop products
|
|
INSERT INTO public.shop_products (slug, name, description, price, category, image_url, affiliate_url, affiliate_label, kinks)
|
|
VALUES
|
|
('pistol-pete-jock', 'Pistol Pete Jockstrap', 'Premium mesh jock with contoured pouch.', 24.99, 'jocks-harness',
|
|
'https://placehold.co/400x400/1a1220/ff2d6b?text=Pistol+Pete+Jock', 'https://www.adammale.com', 'Buy on AdamMale', '{underwear,cruising}'),
|
|
('rush-poppers', 'Rush Poppers', 'Classic amyl nitrite formula. 10ml bottle.', 18.99, 'poppers-lube',
|
|
'https://placehold.co/400x400/1a1220/8b2fc9?text=Rush+Poppers', 'https://www.adammale.com', 'Buy on AdamMale', '{pnp}'),
|
|
('10in-dildo', '10" Realistic Dildo', 'Veined silicone, suction cup base.', 49.99, 'dildos-plugs',
|
|
'https://placehold.co/400x400/1a1220/ff2d6b?text=10in+Dildo', 'https://www.adammale.com', 'Buy on AdamMale', '{anal}'); |