Deck ratings (comment + popup), remove Published badge from My decks

This commit is contained in:
gitea
2026-02-13 19:28:29 +01:00
parent 9eede674b6
commit 32c786fa24
31 changed files with 3618 additions and 140 deletions
@@ -1,5 +1,8 @@
-- Schema for app tables (Supabase API uses db.schema: 'omotomo' in config)
CREATE SCHEMA IF NOT EXISTS omotomo;
-- Decks table (metadata + config for Decky quiz/flashcard app)
CREATE TABLE IF NOT EXISTS public.decks (
CREATE TABLE IF NOT EXISTS omotomo.decks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
title text NOT NULL,
@@ -21,9 +24,9 @@ CREATE TABLE IF NOT EXISTS public.decks (
);
-- Questions table (deck content)
CREATE TABLE IF NOT EXISTS public.questions (
CREATE TABLE IF NOT EXISTS omotomo.questions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
deck_id uuid NOT NULL REFERENCES public.decks(id) ON DELETE CASCADE,
deck_id uuid NOT NULL REFERENCES omotomo.decks(id) ON DELETE CASCADE,
sort_order int NOT NULL DEFAULT 0,
prompt text NOT NULL,
explanation text,
@@ -31,12 +34,12 @@ CREATE TABLE IF NOT EXISTS public.questions (
correct_answer_indices jsonb NOT NULL DEFAULT '[]'::jsonb
);
CREATE INDEX IF NOT EXISTS idx_questions_deck_id ON public.questions(deck_id);
CREATE INDEX IF NOT EXISTS idx_decks_owner_id ON public.decks(owner_id);
CREATE INDEX IF NOT EXISTS idx_decks_published ON public.decks(published) WHERE published = true;
CREATE INDEX IF NOT EXISTS idx_questions_deck_id ON omotomo.questions(deck_id);
CREATE INDEX IF NOT EXISTS idx_decks_owner_id ON omotomo.decks(owner_id);
CREATE INDEX IF NOT EXISTS idx_decks_published ON omotomo.decks(published) WHERE published = true;
-- updated_at trigger for decks
CREATE OR REPLACE FUNCTION public.set_decks_updated_at()
CREATE OR REPLACE FUNCTION omotomo.set_decks_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
@@ -44,74 +47,82 @@ BEGIN
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS decks_updated_at ON public.decks;
DROP TRIGGER IF EXISTS decks_updated_at ON omotomo.decks;
CREATE TRIGGER decks_updated_at
BEFORE UPDATE ON public.decks
BEFORE UPDATE ON omotomo.decks
FOR EACH ROW
EXECUTE PROCEDURE public.set_decks_updated_at();
EXECUTE PROCEDURE omotomo.set_decks_updated_at();
-- RLS
ALTER TABLE public.decks ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.questions ENABLE ROW LEVEL SECURITY;
ALTER TABLE omotomo.decks ENABLE ROW LEVEL SECURITY;
ALTER TABLE omotomo.questions ENABLE ROW LEVEL SECURITY;
-- decks: SELECT if owner or published; mutate only if owner
CREATE POLICY decks_select ON public.decks
DROP POLICY IF EXISTS decks_select ON omotomo.decks;
DROP POLICY IF EXISTS decks_insert ON omotomo.decks;
DROP POLICY IF EXISTS decks_update ON omotomo.decks;
DROP POLICY IF EXISTS decks_delete ON omotomo.decks;
CREATE POLICY decks_select ON omotomo.decks
FOR SELECT
USING (owner_id = auth.uid() OR published = true);
CREATE POLICY decks_insert ON public.decks
CREATE POLICY decks_insert ON omotomo.decks
FOR INSERT
WITH CHECK (owner_id = auth.uid());
CREATE POLICY decks_update ON public.decks
CREATE POLICY decks_update ON omotomo.decks
FOR UPDATE
USING (owner_id = auth.uid())
WITH CHECK (owner_id = auth.uid());
CREATE POLICY decks_delete ON public.decks
CREATE POLICY decks_delete ON omotomo.decks
FOR DELETE
USING (owner_id = auth.uid());
-- questions: SELECT if deck is visible; mutate only if deck is owned
CREATE POLICY questions_select ON public.questions
DROP POLICY IF EXISTS questions_select ON omotomo.questions;
DROP POLICY IF EXISTS questions_insert ON omotomo.questions;
DROP POLICY IF EXISTS questions_update ON omotomo.questions;
DROP POLICY IF EXISTS questions_delete ON omotomo.questions;
CREATE POLICY questions_select ON omotomo.questions
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM public.decks d
SELECT 1 FROM omotomo.decks d
WHERE d.id = questions.deck_id
AND (d.owner_id = auth.uid() OR d.published = true)
)
);
CREATE POLICY questions_insert ON public.questions
CREATE POLICY questions_insert ON omotomo.questions
FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM public.decks d
SELECT 1 FROM omotomo.decks d
WHERE d.id = questions.deck_id AND d.owner_id = auth.uid()
)
);
CREATE POLICY questions_update ON public.questions
CREATE POLICY questions_update ON omotomo.questions
FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM public.decks d
SELECT 1 FROM omotomo.decks d
WHERE d.id = questions.deck_id AND d.owner_id = auth.uid()
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.decks d
SELECT 1 FROM omotomo.decks d
WHERE d.id = questions.deck_id AND d.owner_id = auth.uid()
)
);
CREATE POLICY questions_delete ON public.questions
CREATE POLICY questions_delete ON omotomo.questions
FOR DELETE
USING (
EXISTS (
SELECT 1 FROM public.decks d
SELECT 1 FROM omotomo.decks d
WHERE d.id = questions.deck_id AND d.owner_id = auth.uid()
)
);
@@ -0,0 +1,98 @@
-- Profiles: public display info for users (creator names on community decks)
CREATE TABLE IF NOT EXISTS omotomo.profiles (
id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
display_name text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE omotomo.profiles ENABLE ROW LEVEL SECURITY;
-- Anyone can read profiles (to show creator names)
DROP POLICY IF EXISTS profiles_select ON omotomo.profiles;
DROP POLICY IF EXISTS profiles_insert ON omotomo.profiles;
DROP POLICY IF EXISTS profiles_update ON omotomo.profiles;
DROP POLICY IF EXISTS profiles_delete ON omotomo.profiles;
CREATE POLICY profiles_select ON omotomo.profiles
FOR SELECT USING (true);
-- Users can insert/update/delete only their own profile
CREATE POLICY profiles_insert ON omotomo.profiles
FOR INSERT WITH CHECK (auth.uid() = id);
CREATE POLICY profiles_update ON omotomo.profiles
FOR UPDATE USING (auth.uid() = id) WITH CHECK (auth.uid() = id);
CREATE POLICY profiles_delete ON omotomo.profiles
FOR DELETE USING (auth.uid() = id);
-- Create profile when a new user signs up (Supabase Auth)
CREATE OR REPLACE FUNCTION omotomo.handle_new_user()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = omotomo, public
AS $$
BEGIN
INSERT INTO omotomo.profiles (id, display_name)
VALUES (
NEW.id,
COALESCE(
NEW.raw_user_meta_data->>'full_name',
NEW.raw_user_meta_data->>'name',
split_part(NEW.email, '@', 1),
'User'
)
)
ON CONFLICT (id) DO NOTHING;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION omotomo.handle_new_user();
-- Backfill existing users: run in Supabase Dashboard SQL Editor (Service role) if you have users already:
-- INSERT INTO omotomo.profiles (id, display_name)
-- SELECT id, COALESCE(raw_user_meta_data->>'full_name', raw_user_meta_data->>'name', split_part(email, '@', 1), 'User')
-- FROM auth.users ON CONFLICT (id) DO NOTHING;
-- Deck ratings: one rating (15 stars) per user per deck
CREATE TABLE IF NOT EXISTS omotomo.deck_ratings (
deck_id uuid NOT NULL REFERENCES omotomo.decks(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
rating smallint NOT NULL CHECK (rating >= 1 AND rating <= 5),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (deck_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_deck_ratings_deck_id ON omotomo.deck_ratings(deck_id);
ALTER TABLE omotomo.deck_ratings ENABLE ROW LEVEL SECURITY;
-- Anyone can read ratings (for showing averages on community)
DROP POLICY IF EXISTS deck_ratings_select ON omotomo.deck_ratings;
DROP POLICY IF EXISTS deck_ratings_insert ON omotomo.deck_ratings;
DROP POLICY IF EXISTS deck_ratings_update ON omotomo.deck_ratings;
DROP POLICY IF EXISTS deck_ratings_delete ON omotomo.deck_ratings;
CREATE POLICY deck_ratings_select ON omotomo.deck_ratings
FOR SELECT USING (true);
-- Authenticated users can insert/update only their own rating, and not for decks they own
CREATE POLICY deck_ratings_insert ON omotomo.deck_ratings
FOR INSERT WITH CHECK (
auth.uid() = user_id
AND (SELECT d.owner_id FROM omotomo.decks d WHERE d.id = deck_id) IS DISTINCT FROM auth.uid()
);
CREATE POLICY deck_ratings_update ON omotomo.deck_ratings
FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (
auth.uid() = user_id
AND (SELECT d.owner_id FROM omotomo.decks d WHERE d.id = deck_id) IS DISTINCT FROM auth.uid()
);
CREATE POLICY deck_ratings_delete ON omotomo.deck_ratings
FOR DELETE USING (auth.uid() = user_id);
@@ -0,0 +1,56 @@
-- Add avatar_url to profiles (schema omotomo)
ALTER TABLE omotomo.profiles
ADD COLUMN IF NOT EXISTS avatar_url text;
-- Storage bucket for avatars (public read; uploads scoped to user folder).
-- If the INSERT fails (e.g. different storage schema), create the bucket in Supabase Dashboard:
-- Storage → New bucket → name "avatars", Public bucket ON, File size limit 2MB, Allowed MIME types: image/jpeg, image/png, image/gif, image/webp
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'avatars',
'avatars',
true,
2097152,
ARRAY['image/jpeg', 'image/png', 'image/gif', 'image/webp']
)
ON CONFLICT (id) DO NOTHING;
-- RLS: allow authenticated users to upload only to their own folder (avatars/<user_id>/...)
-- Use auth.jwt()->>'sub' to match the folder name (JWT sub is the user id string).
CREATE POLICY "Users can upload own avatar"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = (auth.jwt()->>'sub')
);
CREATE POLICY "Users can update own avatar"
ON storage.objects
FOR UPDATE
TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = (auth.jwt()->>'sub')
)
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = (auth.jwt()->>'sub')
);
CREATE POLICY "Users can delete own avatar"
ON storage.objects
FOR DELETE
TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = (auth.jwt()->>'sub')
);
-- Public read for avatars (bucket is public)
CREATE POLICY "Avatar images are publicly readable"
ON storage.objects
FOR SELECT
TO public
USING (bucket_id = 'avatars');
@@ -0,0 +1,3 @@
-- Mark decks that were added from the community (no Publish in UI)
ALTER TABLE omotomo.decks
ADD COLUMN IF NOT EXISTS copied_from_deck_id uuid NULL;
@@ -0,0 +1,29 @@
-- Add optional comment to deck ratings (schema omotomo).
-- If deck_ratings doesn't exist (earlier migration not run), create the full table including comment.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'omotomo' AND tablename = 'deck_ratings') THEN
CREATE TABLE omotomo.deck_ratings (
deck_id uuid NOT NULL REFERENCES omotomo.decks(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
rating smallint NOT NULL CHECK (rating >= 1 AND rating <= 5),
created_at timestamptz NOT NULL DEFAULT now(),
comment text,
PRIMARY KEY (deck_id, user_id)
);
CREATE INDEX idx_deck_ratings_deck_id ON omotomo.deck_ratings(deck_id);
ALTER TABLE omotomo.deck_ratings ENABLE ROW LEVEL SECURITY;
CREATE POLICY deck_ratings_select ON omotomo.deck_ratings FOR SELECT USING (true);
CREATE POLICY deck_ratings_insert ON omotomo.deck_ratings FOR INSERT WITH CHECK (
auth.uid() = user_id
AND (SELECT d.owner_id FROM omotomo.decks d WHERE d.id = deck_id) IS DISTINCT FROM auth.uid()
);
CREATE POLICY deck_ratings_update ON omotomo.deck_ratings FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (
auth.uid() = user_id
AND (SELECT d.owner_id FROM omotomo.decks d WHERE d.id = deck_id) IS DISTINCT FROM auth.uid()
);
CREATE POLICY deck_ratings_delete ON omotomo.deck_ratings FOR DELETE USING (auth.uid() = user_id);
ELSE
ALTER TABLE omotomo.deck_ratings ADD COLUMN IF NOT EXISTS comment text;
END IF;
END $$;
@@ -0,0 +1,10 @@
-- Run this in Supabase SQL Editor if you get "column profiles.avatar_url does not exist"
-- Use the schema where your profiles table lives (omotomo or public).
-- If your app uses schema omotomo (db: { schema: 'omotomo' }):
ALTER TABLE omotomo.profiles
ADD COLUMN IF NOT EXISTS avatar_url text;
-- If your profiles table is in public schema instead, run this instead:
-- ALTER TABLE public.profiles
-- ADD COLUMN IF NOT EXISTS avatar_url text;
@@ -0,0 +1,5 @@
-- Run this in Supabase SQL Editor if you get "column decks.copied_from_deck_id does not exist"
-- Uses schema omotomo (match your app's db.schema).
ALTER TABLE omotomo.decks
ADD COLUMN IF NOT EXISTS copied_from_deck_id uuid NULL;
@@ -0,0 +1,37 @@
-- Run this in Supabase SQL Editor if you get "Bucket not found" when saving profile with avatar.
-- If the INSERT fails (e.g. "relation storage.buckets does not exist" or column errors),
-- create the bucket in Dashboard: Storage → New bucket → name "avatars", Public ON.
-- 1) Create the bucket
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'avatars',
'avatars',
true,
2097152,
ARRAY['image/jpeg', 'image/png', 'image/gif', 'image/webp']
)
ON CONFLICT (id) DO NOTHING;
-- 2) RLS policies so users can upload to their own folder and everyone can read
DROP POLICY IF EXISTS "Users can upload own avatar" ON storage.objects;
DROP POLICY IF EXISTS "Users can update own avatar" ON storage.objects;
DROP POLICY IF EXISTS "Users can delete own avatar" ON storage.objects;
DROP POLICY IF EXISTS "Avatar images are publicly readable" ON storage.objects;
CREATE POLICY "Users can upload own avatar"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'avatars' AND (storage.foldername(name))[1] = (auth.jwt()->>'sub'));
CREATE POLICY "Users can update own avatar"
ON storage.objects FOR UPDATE TO authenticated
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = (auth.jwt()->>'sub'))
WITH CHECK (bucket_id = 'avatars' AND (storage.foldername(name))[1] = (auth.jwt()->>'sub'));
CREATE POLICY "Users can delete own avatar"
ON storage.objects FOR DELETE TO authenticated
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = (auth.jwt()->>'sub'));
CREATE POLICY "Avatar images are publicly readable"
ON storage.objects FOR SELECT TO public
USING (bucket_id = 'avatars');
@@ -0,0 +1,37 @@
-- Run this in Supabase SQL Editor if you get 403 "new row violates row-level security policy"
-- when uploading an avatar. Fixes INSERT/UPDATE/DELETE policies to use JWT sub for folder match.
DROP POLICY IF EXISTS "Users can upload own avatar" ON storage.objects;
DROP POLICY IF EXISTS "Users can update own avatar" ON storage.objects;
DROP POLICY IF EXISTS "Users can delete own avatar" ON storage.objects;
DROP POLICY IF EXISTS "Avatar images are publicly readable" ON storage.objects;
-- First folder in path must equal the authenticated user's id (from JWT sub)
CREATE POLICY "Users can upload own avatar"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = (auth.jwt()->>'sub')
);
CREATE POLICY "Users can update own avatar"
ON storage.objects FOR UPDATE TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = (auth.jwt()->>'sub')
)
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = (auth.jwt()->>'sub')
);
CREATE POLICY "Users can delete own avatar"
ON storage.objects FOR DELETE TO authenticated
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = (auth.jwt()->>'sub')
);
CREATE POLICY "Avatar images are publicly readable"
ON storage.objects FOR SELECT TO public
USING (bucket_id = 'avatars');
@@ -0,0 +1,26 @@
-- Run this in Supabase SQL Editor if you get "permission denied for table profiles"
-- 1) Grants the Supabase API roles access to the omotomo schema and profiles table.
-- 2) Ensures RLS policies exist on omotomo.profiles (skip policy creation if you already have them).
-- Allow anon and authenticated roles to use the omotomo schema
GRANT USAGE ON SCHEMA omotomo TO anon, authenticated;
-- Allow reading and writing profiles (RLS policies control which rows)
GRANT SELECT, INSERT, UPDATE, DELETE ON omotomo.profiles TO anon, authenticated;
-- RLS: ensure the table is protected and policies allow the right access
ALTER TABLE omotomo.profiles ENABLE ROW LEVEL SECURITY;
-- Drop existing policies if you need to recreate (optional; remove the DROP lines if policies already work)
DROP POLICY IF EXISTS profiles_select ON omotomo.profiles;
DROP POLICY IF EXISTS profiles_insert ON omotomo.profiles;
DROP POLICY IF EXISTS profiles_update ON omotomo.profiles;
DROP POLICY IF EXISTS profiles_delete ON omotomo.profiles;
-- Anyone can read profiles (e.g. show creator names on community decks)
CREATE POLICY profiles_select ON omotomo.profiles FOR SELECT USING (true);
-- Users can insert/update/delete only their own profile (id = auth.uid())
CREATE POLICY profiles_insert ON omotomo.profiles FOR INSERT WITH CHECK (auth.uid() = id);
CREATE POLICY profiles_update ON omotomo.profiles FOR UPDATE USING (auth.uid() = id) WITH CHECK (auth.uid() = id);
CREATE POLICY profiles_delete ON omotomo.profiles FOR DELETE USING (auth.uid() = id);