-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsupabase-schema-safe.sql
More file actions
421 lines (362 loc) · 15.9 KB
/
supabase-schema-safe.sql
File metadata and controls
421 lines (362 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
-- Safe Supabase Schema Setup (Idempotent - can run multiple times)
-- This script uses IF NOT EXISTS and handles existing objects gracefully
-- Users table (extends Supabase auth.users)
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID REFERENCES auth.users PRIMARY KEY,
email TEXT,
name TEXT,
bio TEXT,
avatar_url TEXT,
social_links JSONB DEFAULT '{}',
is_instructor BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Add is_instructor and is_admin columns if they don't exist (for existing tables)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'profiles') THEN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'profiles'
AND column_name = 'is_instructor'
) THEN
ALTER TABLE public.profiles ADD COLUMN is_instructor BOOLEAN DEFAULT FALSE;
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'profiles'
AND column_name = 'is_admin'
) THEN
ALTER TABLE public.profiles ADD COLUMN is_admin BOOLEAN DEFAULT FALSE;
END IF;
END IF;
END $$;
-- User progress
CREATE TABLE IF NOT EXISTS public.user_progress (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users ON DELETE CASCADE,
course_id TEXT NOT NULL,
module_id TEXT NOT NULL,
completed BOOLEAN DEFAULT FALSE,
completed_at TIMESTAMPTZ,
progress_percentage INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, course_id, module_id)
);
-- Quiz scores
CREATE TABLE IF NOT EXISTS public.quiz_scores (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users ON DELETE CASCADE,
course_id TEXT NOT NULL,
quiz_id TEXT NOT NULL,
score INTEGER NOT NULL,
max_score INTEGER NOT NULL,
answers JSONB,
completed_at TIMESTAMPTZ DEFAULT NOW()
);
-- Messages/conversations
CREATE TABLE IF NOT EXISTS public.conversations (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user1_id UUID REFERENCES auth.users ON DELETE CASCADE,
user2_id UUID REFERENCES auth.users ON DELETE CASCADE,
type TEXT DEFAULT 'friend',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user1_id, user2_id, type)
);
CREATE TABLE IF NOT EXISTS public.messages (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
conversation_id UUID REFERENCES public.conversations ON DELETE CASCADE,
sender_id UUID REFERENCES auth.users ON DELETE CASCADE,
content TEXT NOT NULL,
type TEXT DEFAULT 'text',
read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Friends/followers
CREATE TABLE IF NOT EXISTS public.friends (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users ON DELETE CASCADE,
friend_id UUID REFERENCES auth.users ON DELETE CASCADE,
status TEXT DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, friend_id)
);
-- Assignments
CREATE TABLE IF NOT EXISTS public.assignments (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users ON DELETE CASCADE,
course_id TEXT NOT NULL,
module_id TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
file_url TEXT,
grade INTEGER,
feedback TEXT,
submitted_at TIMESTAMPTZ DEFAULT NOW(),
graded_at TIMESTAMPTZ
);
-- Course Reviews
CREATE TABLE IF NOT EXISTS public.course_reviews (
id TEXT PRIMARY KEY,
course_id TEXT NOT NULL,
user_id TEXT NOT NULL,
user_name TEXT,
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
comment TEXT,
helpful_count INTEGER DEFAULT 0,
is_visible BOOLEAN DEFAULT TRUE,
verified_purchase BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Forum Threads
CREATE TABLE IF NOT EXISTS public.forum_threads (
id TEXT PRIMARY KEY,
course_id TEXT NOT NULL,
module_id TEXT,
title TEXT NOT NULL,
author_id TEXT NOT NULL,
author_name TEXT,
post_count INTEGER DEFAULT 1,
view_count INTEGER DEFAULT 0,
upvotes INTEGER DEFAULT 0,
is_pinned BOOLEAN DEFAULT FALSE,
is_locked BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Forum Posts
CREATE TABLE IF NOT EXISTS public.forum_posts (
id TEXT PRIMARY KEY,
thread_id TEXT NOT NULL,
author_id TEXT NOT NULL,
author_name TEXT,
content TEXT NOT NULL,
upvotes INTEGER DEFAULT 0,
is_answer BOOLEAN DEFAULT FALSE,
parent_post_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Courses Table (for instructor-created courses)
CREATE TABLE IF NOT EXISTS public.courses (
id TEXT PRIMARY KEY,
instructor_id TEXT NOT NULL,
instructor_name TEXT,
title TEXT NOT NULL,
description TEXT,
subtitle TEXT,
category TEXT DEFAULT 'general',
price DECIMAL(10, 2) DEFAULT 0,
is_free BOOLEAN DEFAULT TRUE,
thumbnail TEXT,
level TEXT DEFAULT 'Beginner',
duration TEXT DEFAULT '8 weeks',
modules INTEGER DEFAULT 8,
modules_data JSONB DEFAULT '[]'::jsonb,
is_published BOOLEAN DEFAULT FALSE,
is_approved BOOLEAN DEFAULT FALSE,
approval_status TEXT DEFAULT 'pending', -- 'pending', 'approved', 'rejected'
admin_notes TEXT,
approved_by TEXT,
approved_at TIMESTAMPTZ,
enrollment_count INTEGER DEFAULT 0,
rating DECIMAL(3, 2) DEFAULT 0,
review_count INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Add approval columns if table already exists
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'courses') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'courses' AND column_name = 'is_approved') THEN
ALTER TABLE public.courses ADD COLUMN is_approved BOOLEAN DEFAULT FALSE;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'courses' AND column_name = 'approval_status') THEN
ALTER TABLE public.courses ADD COLUMN approval_status TEXT DEFAULT 'pending';
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'courses' AND column_name = 'admin_notes') THEN
ALTER TABLE public.courses ADD COLUMN admin_notes TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'courses' AND column_name = 'approved_by') THEN
ALTER TABLE public.courses ADD COLUMN approved_by TEXT;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'courses' AND column_name = 'approved_at') THEN
ALTER TABLE public.courses ADD COLUMN approved_at TIMESTAMPTZ;
END IF;
END IF;
END $$;
-- Enable Row Level Security (idempotent - safe to run multiple times)
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_progress ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.quiz_scores ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.conversations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.friends ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.assignments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.course_reviews ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.forum_threads ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.forum_posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.courses ENABLE ROW LEVEL SECURITY;
-- Drop existing policies if they exist (for idempotency)
DROP POLICY IF EXISTS "Users can view own profile" ON public.profiles;
DROP POLICY IF EXISTS "Users can update own profile" ON public.profiles;
DROP POLICY IF EXISTS "Users can view own progress" ON public.user_progress;
DROP POLICY IF EXISTS "Users can update own progress" ON public.user_progress;
DROP POLICY IF EXISTS "Users can view own quiz scores" ON public.quiz_scores;
DROP POLICY IF EXISTS "Users can view own conversations" ON public.conversations;
DROP POLICY IF EXISTS "Users can view own messages" ON public.messages;
DROP POLICY IF EXISTS "Users can send messages" ON public.messages;
DROP POLICY IF EXISTS "Users can manage own friends" ON public.friends;
DROP POLICY IF EXISTS "Users can manage own assignments" ON public.assignments;
-- RLS Policies: Users can only see/edit their own data
CREATE POLICY "Users can view own profile" ON public.profiles
FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can update own profile" ON public.profiles
FOR UPDATE USING (auth.uid() = id);
CREATE POLICY "Users can view own progress" ON public.user_progress
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can update own progress" ON public.user_progress
FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "Users can view own quiz scores" ON public.quiz_scores
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can view own conversations" ON public.conversations
FOR SELECT USING (auth.uid() = user1_id OR auth.uid() = user2_id);
CREATE POLICY "Users can view own messages" ON public.messages
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.conversations
WHERE conversations.id = messages.conversation_id
AND (conversations.user1_id = auth.uid() OR conversations.user2_id = auth.uid())
)
);
CREATE POLICY "Users can send messages" ON public.messages
FOR INSERT WITH CHECK (auth.uid() = sender_id);
CREATE POLICY "Users can manage own friends" ON public.friends
FOR ALL USING (auth.uid() = user_id);
CREATE POLICY "Users can manage own assignments" ON public.assignments
FOR ALL USING (auth.uid() = user_id);
-- Course Reviews Policies: Anyone can view, authenticated users can create/update own
DROP POLICY IF EXISTS "Anyone can view course reviews" ON public.course_reviews;
DROP POLICY IF EXISTS "Users can create own reviews" ON public.course_reviews;
DROP POLICY IF EXISTS "Users can update own reviews" ON public.course_reviews;
DROP POLICY IF EXISTS "Users can delete own reviews" ON public.course_reviews;
CREATE POLICY "Anyone can view course reviews" ON public.course_reviews
FOR SELECT USING (is_visible = TRUE);
CREATE POLICY "Users can create own reviews" ON public.course_reviews
FOR INSERT WITH CHECK (auth.uid()::text = user_id);
CREATE POLICY "Users can update own reviews" ON public.course_reviews
FOR UPDATE USING (auth.uid()::text = user_id);
CREATE POLICY "Users can delete own reviews" ON public.course_reviews
FOR DELETE USING (auth.uid()::text = user_id);
-- Forum Threads Policies: Anyone can view, authenticated users can create/update own
DROP POLICY IF EXISTS "Anyone can view forum threads" ON public.forum_threads;
DROP POLICY IF EXISTS "Users can create threads" ON public.forum_threads;
DROP POLICY IF EXISTS "Users can update own threads" ON public.forum_threads;
DROP POLICY IF EXISTS "Users can delete own threads" ON public.forum_threads;
CREATE POLICY "Anyone can view forum threads" ON public.forum_threads
FOR SELECT USING (true);
CREATE POLICY "Users can create threads" ON public.forum_threads
FOR INSERT WITH CHECK (auth.uid()::text = author_id);
CREATE POLICY "Users can update own threads" ON public.forum_threads
FOR UPDATE USING (auth.uid()::text = author_id);
CREATE POLICY "Users can delete own threads" ON public.forum_threads
FOR DELETE USING (auth.uid()::text = author_id);
-- Forum Posts Policies: Anyone can view, authenticated users can create/update own
DROP POLICY IF EXISTS "Anyone can view forum posts" ON public.forum_posts;
DROP POLICY IF EXISTS "Users can create posts" ON public.forum_posts;
DROP POLICY IF EXISTS "Users can update own posts" ON public.forum_posts;
DROP POLICY IF EXISTS "Users can delete own posts" ON public.forum_posts;
CREATE POLICY "Anyone can view forum posts" ON public.forum_posts
FOR SELECT USING (true);
CREATE POLICY "Users can create posts" ON public.forum_posts
FOR INSERT WITH CHECK (auth.uid()::text = author_id);
CREATE POLICY "Users can update own posts" ON public.forum_posts
FOR UPDATE USING (auth.uid()::text = author_id);
CREATE POLICY "Users can delete own posts" ON public.forum_posts
FOR DELETE USING (auth.uid()::text = author_id);
-- Courses Policies: Anyone can view published courses, instructors can manage own courses
DROP POLICY IF EXISTS "Anyone can view published courses" ON public.courses;
DROP POLICY IF EXISTS "Instructors can create courses" ON public.courses;
DROP POLICY IF EXISTS "Instructors can update own courses" ON public.courses;
DROP POLICY IF EXISTS "Instructors can delete own courses" ON public.courses;
CREATE POLICY "Anyone can view published courses" ON public.courses
FOR SELECT USING (is_published = true OR auth.uid()::text = instructor_id);
CREATE POLICY "Instructors can create courses" ON public.courses
FOR INSERT WITH CHECK (auth.uid()::text = instructor_id);
CREATE POLICY "Instructors can update own courses" ON public.courses
FOR UPDATE USING (auth.uid()::text = instructor_id);
CREATE POLICY "Instructors can delete own courses" ON public.courses
FOR DELETE USING (auth.uid()::text = instructor_id);
-- Admin Actions Log Table
CREATE TABLE IF NOT EXISTS public.admin_actions (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
admin_id TEXT NOT NULL,
action_type TEXT NOT NULL, -- 'approve_course', 'reject_course', 'make_admin', 'make_instructor', etc.
target_id TEXT, -- course_id, user_id, etc.
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE public.admin_actions ENABLE ROW LEVEL SECURITY;
-- Admin Actions Policies: Only admins can view/create
DROP POLICY IF EXISTS "Admins can view admin actions" ON public.admin_actions;
DROP POLICY IF EXISTS "Admins can create admin actions" ON public.admin_actions;
CREATE POLICY "Admins can view admin actions" ON public.admin_actions
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.profiles
WHERE profiles.id::text = auth.uid()::text
AND profiles.is_admin = TRUE
)
);
CREATE POLICY "Admins can create admin actions" ON public.admin_actions
FOR INSERT WITH CHECK (
EXISTS (
SELECT 1 FROM public.profiles
WHERE profiles.id::text = auth.uid()::text
AND profiles.is_admin = TRUE
)
);
-- Stripe subscriptions (one row per user, keyed by email)
CREATE TABLE IF NOT EXISTS public.subscriptions (
user_email TEXT PRIMARY KEY,
plan TEXT NOT NULL DEFAULT 'free',
status TEXT NOT NULL DEFAULT 'inactive',
stripe_subscription_id TEXT UNIQUE,
stripe_customer_id TEXT,
start_date TIMESTAMPTZ,
end_date TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS subscriptions_stripe_sub_idx
ON public.subscriptions(stripe_subscription_id);
ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
-- Read-only: a user can read their own subscription via their auth email
DROP POLICY IF EXISTS "Users read own subscription" ON public.subscriptions;
CREATE POLICY "Users read own subscription" ON public.subscriptions
FOR SELECT USING (
user_email = (SELECT email FROM auth.users WHERE id = auth.uid())
);
-- Writes happen only via the service-role webhook (bypasses RLS) — no INSERT/UPDATE policy.
-- Function to auto-create profile on signup
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email, name)
VALUES (NEW.id, NEW.email, COALESCE(NEW.raw_user_meta_data->>'name', NEW.email))
ON CONFLICT (id) DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Drop existing trigger if it exists
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 public.handle_new_user();