ExposedGays: map, shop, auth, Supabase schema, Coolify Dockerfile
This commit is contained in:
45
supabase/functions.sql
Normal file
45
supabase/functions.sql
Normal file
@@ -0,0 +1,45 @@
|
||||
-- Additional Supabase functions for ExposedGays
|
||||
-- Run after schema.sql
|
||||
|
||||
-- Nearby profiles using PostGIS
|
||||
CREATE OR REPLACE FUNCTION public.nearby_profiles(
|
||||
p_lat DOUBLE PRECISION,
|
||||
p_lng DOUBLE PRECISION,
|
||||
p_radius_km DOUBLE PRECISION DEFAULT 10
|
||||
)
|
||||
RETURNS SETOF public.profiles AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT p.*
|
||||
FROM public.profiles p
|
||||
WHERE p.location IS NOT NULL
|
||||
AND p.status != 'offline'
|
||||
AND ST_DWithin(
|
||||
p.location,
|
||||
ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
|
||||
p_radius_km * 1000
|
||||
)
|
||||
ORDER BY p.last_active DESC
|
||||
LIMIT 200;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Nearby cruising spots
|
||||
CREATE OR REPLACE FUNCTION public.nearby_spots(
|
||||
p_lat DOUBLE PRECISION,
|
||||
p_lng DOUBLE PRECISION,
|
||||
p_radius_km DOUBLE PRECISION DEFAULT 25
|
||||
)
|
||||
RETURNS SETOF public.cruising_spots AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT s.*
|
||||
FROM public.cruising_spots s
|
||||
WHERE ST_DWithin(
|
||||
s.location,
|
||||
ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
|
||||
p_radius_km * 1000
|
||||
)
|
||||
ORDER BY s.active_count DESC, s.rating DESC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
246
supabase/schema.sql
Normal file
246
supabase/schema.sql
Normal file
@@ -0,0 +1,246 @@
|
||||
-- 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}');
|
||||
Reference in New Issue
Block a user