diff --git a/lifetimer/.env.example b/lifetimer/.env.example new file mode 100644 index 0000000..e985113 --- /dev/null +++ b/lifetimer/.env.example @@ -0,0 +1,6 @@ +# Supabase Configuration +# Copy this file to .env and fill in your actual values +# Then run the app with: flutter run --dart-define-from-file=.env + +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key-here diff --git a/supabase/.env.example b/supabase/.env.example new file mode 100644 index 0000000..f778cda --- /dev/null +++ b/supabase/.env.example @@ -0,0 +1,5 @@ +# Supabase Configuration +# Copy this file to .env and fill in your actual values + +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=your-anon-key-here diff --git a/supabase/migrations/001_initial_schema.sql b/supabase/migrations/001_initial_schema.sql new file mode 100644 index 0000000..9878d8b --- /dev/null +++ b/supabase/migrations/001_initial_schema.sql @@ -0,0 +1,214 @@ +-- Enable UUID extension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Create users table +CREATE TABLE IF NOT EXISTS public.users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + auth_provider_id TEXT, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + avatar_url TEXT, + bio TEXT, + is_public_profile BOOLEAN DEFAULT false, + countdown_start_date TIMESTAMPTZ, + countdown_end_date TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create goals table +CREATE TABLE IF NOT EXISTS public.goals ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + owner_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + title TEXT NOT NULL, + description TEXT, + progress INTEGER DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), + location_lat DOUBLE PRECISION, + location_lng DOUBLE PRECISION, + location_name TEXT, + image_url TEXT, + completed BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create goal_steps table +CREATE TABLE IF NOT EXISTS public.goal_steps ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + goal_id UUID NOT NULL REFERENCES public.goals(id) ON DELETE CASCADE, + title TEXT NOT NULL, + is_done BOOLEAN DEFAULT false, + order_index INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create followers table +CREATE TABLE IF NOT EXISTS public.followers ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + follower_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(user_id, follower_id) +); + +-- Create activities table +CREATE TABLE IF NOT EXISTS public.activities ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + type TEXT NOT NULL, + payload JSONB, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Create notifications table +CREATE TABLE IF NOT EXISTS public.notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, + type TEXT NOT NULL, + title TEXT NOT NULL, + body TEXT NOT NULL, + scheduled_for TIMESTAMPTZ, + delivered_at TIMESTAMPTZ +); + +-- Create indexes +CREATE INDEX IF NOT EXISTS idx_goals_owner_id ON public.goals(owner_id); +CREATE INDEX IF NOT EXISTS idx_goal_steps_goal_id ON public.goal_steps(goal_id); +CREATE INDEX IF NOT EXISTS idx_followers_user_id ON public.followers(user_id); +CREATE INDEX IF NOT EXISTS idx_followers_follower_id ON public.followers(follower_id); +CREATE INDEX IF NOT EXISTS idx_activities_user_id_created_at ON public.activities(user_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON public.notifications(user_id); + +-- Enable Row Level Security +ALTER TABLE public.users ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.goals ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.goal_steps ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.followers ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.activities ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.notifications ENABLE ROW LEVEL SECURITY; + +-- RLS Policies for users +CREATE POLICY "Users can select their own profile" +ON public.users FOR SELECT +USING (auth.uid() = id); + +CREATE POLICY "Users can update their own profile" +ON public.users FOR UPDATE +USING (auth.uid() = id) +WITH CHECK (auth.uid() = id); + +-- RLS Policies for goals +CREATE POLICY "Users can read their own goals" +ON public.goals FOR SELECT +USING (auth.uid() = owner_id); + +CREATE POLICY "Users can insert their own goals" +ON public.goals FOR INSERT +WITH CHECK (auth.uid() = owner_id); + +CREATE POLICY "Users can update their own goals" +ON public.goals FOR UPDATE +USING (auth.uid() = owner_id) +WITH CHECK (auth.uid() = owner_id); + +CREATE POLICY "Users can delete their own goals" +ON public.goals FOR DELETE +USING (auth.uid() = owner_id); + +-- RLS Policies for goal_steps +CREATE POLICY "Users can read steps for their own goals" +ON public.goal_steps FOR SELECT +USING ( + EXISTS ( + SELECT 1 FROM public.goals g + WHERE g.id = goal_id AND g.owner_id = auth.uid() + ) +); + +CREATE POLICY "Users can insert steps for their own goals" +ON public.goal_steps FOR INSERT +WITH CHECK ( + EXISTS ( + SELECT 1 FROM public.goals g + WHERE g.id = goal_id AND g.owner_id = auth.uid() + ) +); + +CREATE POLICY "Users can update steps for their own goals" +ON public.goal_steps FOR UPDATE +USING ( + EXISTS ( + SELECT 1 FROM public.goals g + WHERE g.id = goal_id AND g.owner_id = auth.uid() + ) +) +WITH CHECK ( + EXISTS ( + SELECT 1 FROM public.goals g + WHERE g.id = goal_id AND g.owner_id = auth.uid() + ) +); + +CREATE POLICY "Users can delete steps for their own goals" +ON public.goal_steps FOR DELETE +USING ( + EXISTS ( + SELECT 1 FROM public.goals g + WHERE g.id = goal_id AND g.owner_id = auth.uid() + ) +); + +-- RLS Policies for followers +CREATE POLICY "Users can read their follower relationships" +ON public.followers FOR SELECT +USING (auth.uid() = user_id OR auth.uid() = follower_id); + +CREATE POLICY "Users can follow others" +ON public.followers FOR INSERT +WITH CHECK (auth.uid() = follower_id); + +CREATE POLICY "Users can unfollow or remove followers" +ON public.followers FOR DELETE +USING (auth.uid() = user_id OR auth.uid() = follower_id); + +-- RLS Policies for activities +CREATE POLICY "Users can read their own activity" +ON public.activities FOR SELECT +USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert their own activity events" +ON public.activities FOR INSERT +WITH CHECK (auth.uid() = user_id); + +-- RLS Policies for notifications +CREATE POLICY "Users can read their own notifications" +ON public.notifications FOR SELECT +USING (auth.uid() = user_id); + +CREATE POLICY "Users can receive their own notifications" +ON public.notifications FOR INSERT +WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can delete their own notifications" +ON public.notifications FOR DELETE +USING (auth.uid() = user_id); + +-- Function to update updated_at timestamp +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Triggers for updated_at +CREATE TRIGGER update_users_updated_at + BEFORE UPDATE ON public.users + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_goals_updated_at + BEFORE UPDATE ON public.goals + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); diff --git a/supabase/migrations/002_public_profile_view.sql b/supabase/migrations/002_public_profile_view.sql new file mode 100644 index 0000000..41ce745 --- /dev/null +++ b/supabase/migrations/002_public_profile_view.sql @@ -0,0 +1,58 @@ +-- Create a view for public profile information +-- This view exposes only non-sensitive information for public profiles +CREATE OR REPLACE VIEW public.public_profiles AS +SELECT + id, + username, + avatar_url, + bio, + is_public_profile, + countdown_start_date, + countdown_end_date, + created_at +FROM public.users +WHERE is_public_profile = true; + +-- Grant access to the view for authenticated users +GRANT SELECT ON public.public_profiles TO authenticated; + +-- Function to get public leaderboard +CREATE OR REPLACE FUNCTION get_leaderboard(sort_by TEXT DEFAULT 'created_at', limit_count INTEGER DEFAULT 50) +RETURNS TABLE ( + id UUID, + username TEXT, + avatar_url TEXT, + bio TEXT, + countdown_start_date TIMESTAMPTZ, + countdown_end_date TIMESTAMPTZ, + goals_completed_count INTEGER +) AS $$ +BEGIN + RETURN QUERY + SELECT + u.id, + u.username, + u.avatar_url, + u.bio, + u.countdown_start_date, + u.countdown_end_date, + COALESCE( + (SELECT COUNT(*) FROM public.goals g WHERE g.owner_id = u.id AND g.completed = true), + 0 + ) as goals_completed_count + FROM public.users u + WHERE u.is_public_profile = true + ORDER BY + CASE sort_by + WHEN 'goals_completed' THEN COALESCE( + (SELECT COUNT(*) FROM public.goals g WHERE g.owner_id = u.id AND g.completed = true), + 0 + ) + ELSE u.created_at + END DESC + LIMIT limit_count; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Grant execute on the function +GRANT EXECUTE ON FUNCTION get_leaderboard(TEXT, INTEGER) TO authenticated; diff --git a/supabase/migrations/003_activity_triggers.sql b/supabase/migrations/003_activity_triggers.sql new file mode 100644 index 0000000..ed789d7 --- /dev/null +++ b/supabase/migrations/003_activity_triggers.sql @@ -0,0 +1,75 @@ +-- Function to automatically log goal creation activity +CREATE OR REPLACE FUNCTION log_goal_created() +RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO public.activities (user_id, type, payload) + VALUES ( + NEW.owner_id, + 'goal_created', + jsonb_build_object( + 'goal_id', NEW.id, + 'goal_title', NEW.title + ) + ); + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Trigger for goal creation +CREATE TRIGGER on_goal_created + AFTER INSERT ON public.goals + FOR EACH ROW + EXECUTE FUNCTION log_goal_created(); + +-- Function to automatically log goal completion activity +CREATE OR REPLACE FUNCTION log_goal_completed() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.completed = true AND (OLD.completed IS NULL OR OLD.completed = false) THEN + INSERT INTO public.activities (user_id, type, payload) + VALUES ( + NEW.owner_id, + 'goal_completed', + jsonb_build_object( + 'goal_id', NEW.id, + 'goal_title', NEW.title, + 'progress', NEW.progress + ) + ); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Trigger for goal completion +CREATE TRIGGER on_goal_completed + AFTER UPDATE ON public.goals + FOR EACH ROW + WHEN (NEW.completed IS DISTINCT FROM OLD.completed) + EXECUTE FUNCTION log_goal_completed(); + +-- Function to automatically log countdown start activity +CREATE OR REPLACE FUNCTION log_countdown_started() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.countdown_start_date IS NOT NULL AND (OLD.countdown_start_date IS NULL) THEN + INSERT INTO public.activities (user_id, type, payload) + VALUES ( + NEW.id, + 'countdown_started', + jsonb_build_object( + 'start_date', NEW.countdown_start_date, + 'end_date', NEW.countdown_end_date + ) + ); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Trigger for countdown start +CREATE TRIGGER on_countdown_started + AFTER UPDATE ON public.users + FOR EACH ROW + WHEN (NEW.countdown_start_date IS DISTINCT FROM OLD.countdown_start_date) + EXECUTE FUNCTION log_countdown_started();