-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsupabase_schema.sql
More file actions
454 lines (408 loc) · 20.3 KB
/
supabase_schema.sql
File metadata and controls
454 lines (408 loc) · 20.3 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
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
-- =============================================
-- SUPABASE SCHEMA — Portfolio Database
-- =============================================
-- Security Model:
-- ✅ Anyone can READ (public portfolio)
-- ✅ Only the site owner (first registered user) can WRITE
-- ✅ SQL Injection impossible (Supabase PostgREST)
-- ✅ Sign-up disabled in Supabase Auth settings
-- =============================================
-- Homepage Section Ordering
CREATE TABLE public.section_order (
section_id text PRIMARY KEY,
order_index integer DEFAULT 0
);
-- About Me (profile info, bio, stats, quote)
CREATE TABLE public.about_me (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
name text,
role text,
hero_tagline text,
bio text,
profile_photo_url text,
started_coding_year integer,
projects_count integer,
years_experience integer,
quote_text text,
quote_author text,
stat_1_value text,
stat_1_label text,
stat_2_value text,
stat_2_label text,
stat_3_value text,
stat_3_label text,
show_quote boolean DEFAULT true,
show_stats boolean DEFAULT true,
show_profile_photo boolean DEFAULT true,
-- Translations
hero_tagline_tr text,
hero_tagline_de text,
hero_tagline_es text,
bio_tr text,
bio_de text,
bio_es text,
role_tr text,
role_de text,
role_es text,
quote_text_tr text,
quote_text_de text,
quote_text_es text,
stat_1_label_tr text,
stat_1_label_de text,
stat_1_label_es text,
stat_2_label_tr text,
stat_2_label_de text,
stat_2_label_es text,
stat_3_label_tr text,
stat_3_label_de text,
stat_3_label_es text,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Skill Categories
CREATE TABLE public.skill_categories (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
title text NOT NULL,
skills jsonb DEFAULT '[]'::jsonb,
title_tr text,
title_de text,
title_es text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Experiences (work history)
CREATE TABLE public.experiences (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
title text NOT NULL,
company text NOT NULL,
location text,
start_date text,
end_date text,
is_current boolean DEFAULT false,
logo_url text,
description text,
-- Translations
title_tr text,
title_de text,
title_es text,
description_tr text,
description_de text,
description_es text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Educations
CREATE TABLE public.educations (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
university text NOT NULL,
degree text,
major text,
location text,
start_date text,
end_date text,
logo_url text,
is_current boolean DEFAULT false,
gpa text,
-- Translations
university_tr text,
university_de text,
university_es text,
degree_tr text,
degree_de text,
degree_es text,
major_tr text,
major_de text,
major_es text,
location_tr text,
location_de text,
location_es text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Languages (proficiency levels 0-100)
CREATE TABLE public.languages (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
name text NOT NULL,
level text,
name_tr text,
name_de text,
name_es text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Activities / Leadership
CREATE TABLE public.activities (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
organization text NOT NULL,
role text NOT NULL,
start_date text,
end_date text,
logo_url text,
description text,
link_url text,
-- Translations
organization_tr text,
organization_de text,
organization_es text,
role_tr text,
role_de text,
role_es text,
description_tr text,
description_de text,
description_es text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Certifications
CREATE TABLE public.certifications (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
name text NOT NULL,
issuer text NOT NULL,
issue_date text,
icon_url text,
link_url text,
-- Translations
name_tr text,
name_de text,
name_es text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Certification Skills (Junction Table)
CREATE TABLE public.certification_skills (
certification_id uuid NOT NULL REFERENCES public.certifications(id) ON DELETE CASCADE,
skill_category_id uuid NOT NULL REFERENCES public.skill_categories(id) ON DELETE CASCADE,
CONSTRAINT certification_skills_pkey PRIMARY KEY (certification_id, skill_category_id)
);
-- Projects (Works)
CREATE TABLE public.projects (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
title text NOT NULL,
description text,
link text,
github text,
image text,
tags jsonb DEFAULT '[]'::jsonb,
title_tr text,
description_tr text,
title_de text,
description_de text,
title_es text,
description_es text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Project Images
CREATE TABLE public.project_images (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE,
image_url text NOT NULL,
caption text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Blogs
CREATE TABLE public.blogs (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
title text NOT NULL,
excerpt text,
content text,
date text,
read_time text,
title_tr text,
excerpt_tr text,
content_tr text,
title_de text,
excerpt_de text,
content_de text,
title_es text,
excerpt_es text,
content_es text,
linked_project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
linked_experience_id uuid REFERENCES public.experiences(id) ON DELETE SET NULL,
linked_education_id uuid REFERENCES public.educations(id) ON DELETE SET NULL,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Social Links (for dock navigation)
CREATE TABLE public.social_links (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
platform text NOT NULL,
url text NOT NULL,
icon text,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Contact Emails (for contact popup)
-- Migration: Add translation columns if table already exists without them
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'contact_emails') THEN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'contact_emails' AND column_name = 'label_tr') THEN
ALTER TABLE public.contact_emails ADD COLUMN label_tr text;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'contact_emails' AND column_name = 'label_de') THEN
ALTER TABLE public.contact_emails ADD COLUMN label_de text;
END IF;
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'contact_emails' AND column_name = 'label_es') THEN
ALTER TABLE public.contact_emails ADD COLUMN label_es text;
END IF;
END IF;
END $$;
CREATE TABLE IF NOT EXISTS public.contact_emails (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
label text NOT NULL,
label_tr text,
label_de text,
label_es text,
email text NOT NULL,
order_index integer DEFAULT 0,
created_at timestamp with time zone DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- =============================================
-- DATA VALIDATION CONSTRAINTS (Security Hardening)
-- =============================================
-- URL Validations to prevent stored XSS or bad links
ALTER TABLE public.about_me ADD CONSTRAINT check_about_photo_url CHECK (profile_photo_url IS NULL OR profile_photo_url ~ '^https?://|^/[^/]');
ALTER TABLE public.experiences ADD CONSTRAINT check_exp_logo_url CHECK (logo_url IS NULL OR logo_url ~ '^https?://|^/[^/]');
ALTER TABLE public.educations ADD CONSTRAINT check_edu_logo_url CHECK (logo_url IS NULL OR logo_url ~ '^https?://|^/[^/]');
ALTER TABLE public.activities ADD CONSTRAINT check_act_logo_url CHECK (logo_url IS NULL OR logo_url ~ '^https?://|^/[^/]');
ALTER TABLE public.activities ADD CONSTRAINT check_act_link_url CHECK (link_url IS NULL OR link_url ~ '^https?://|^/[^/]');
ALTER TABLE public.certifications ADD CONSTRAINT check_cert_icon_url CHECK (icon_url IS NULL OR icon_url ~ '^https?://|^/[^/]');
ALTER TABLE public.certifications ADD CONSTRAINT check_cert_link_url CHECK (link_url IS NULL OR link_url ~ '^https?://|^/[^/]');
ALTER TABLE public.projects ADD CONSTRAINT check_project_link CHECK (link IS NULL OR link ~ '^https?://|^/[^/]');
ALTER TABLE public.projects ADD CONSTRAINT check_project_github CHECK (github IS NULL OR github ~ '^https?://|^/[^/]');
ALTER TABLE public.projects ADD CONSTRAINT check_project_image CHECK (image IS NULL OR image ~ '^https?://|^/[^/]');
ALTER TABLE public.project_images ADD CONSTRAINT check_project_images_url CHECK (image_url ~ '^https?://|^/[^/]');
ALTER TABLE public.social_links ADD CONSTRAINT check_social_url CHECK (url IS NULL OR url ~ '^https?://|^mailto:');
-- Content Length Limits to prevent DoS (payload size)
ALTER TABLE public.about_me ADD CONSTRAINT check_bio_length CHECK (char_length(bio) <= 5000);
ALTER TABLE public.about_me ADD CONSTRAINT check_bio_tr_length CHECK (bio_tr IS NULL OR char_length(bio_tr) <= 5000);
ALTER TABLE public.about_me ADD CONSTRAINT check_bio_de_length CHECK (bio_de IS NULL OR char_length(bio_de) <= 5000);
ALTER TABLE public.about_me ADD CONSTRAINT check_bio_es_length CHECK (bio_es IS NULL OR char_length(bio_es) <= 5000);
ALTER TABLE public.experiences ADD CONSTRAINT check_exp_desc_length CHECK (char_length(description) <= 5000);
ALTER TABLE public.experiences ADD CONSTRAINT check_exp_desc_tr_length CHECK (description_tr IS NULL OR char_length(description_tr) <= 5000);
ALTER TABLE public.experiences ADD CONSTRAINT check_exp_desc_de_length CHECK (description_de IS NULL OR char_length(description_de) <= 5000);
ALTER TABLE public.experiences ADD CONSTRAINT check_exp_desc_es_length CHECK (description_es IS NULL OR char_length(description_es) <= 5000);
ALTER TABLE public.activities ADD CONSTRAINT check_act_desc_length CHECK (char_length(description) <= 5000);
ALTER TABLE public.activities ADD CONSTRAINT check_act_desc_tr_length CHECK (description_tr IS NULL OR char_length(description_tr) <= 5000);
ALTER TABLE public.activities ADD CONSTRAINT check_act_desc_de_length CHECK (description_de IS NULL OR char_length(description_de) <= 5000);
ALTER TABLE public.activities ADD CONSTRAINT check_act_desc_es_length CHECK (description_es IS NULL OR char_length(description_es) <= 5000);
ALTER TABLE public.projects ADD CONSTRAINT check_project_desc_length CHECK (char_length(description) <= 5000);
ALTER TABLE public.projects ADD CONSTRAINT check_project_desc_tr_length CHECK (description_tr IS NULL OR char_length(description_tr) <= 5000);
ALTER TABLE public.projects ADD CONSTRAINT check_project_desc_de_length CHECK (description_de IS NULL OR char_length(description_de) <= 5000);
ALTER TABLE public.projects ADD CONSTRAINT check_project_desc_es_length CHECK (description_es IS NULL OR char_length(description_es) <= 5000);
ALTER TABLE public.blogs ADD CONSTRAINT check_blog_excerpt_length CHECK (char_length(excerpt) <= 2000);
ALTER TABLE public.blogs ADD CONSTRAINT check_blog_content_length CHECK (char_length(content) <= 100000);
-- =============================================
-- ROW LEVEL SECURITY (RLS)
-- =============================================
ALTER TABLE public.about_me ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.section_order ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.skill_categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.experiences ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.educations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.languages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.activities ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.certifications ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.certification_skills ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.project_images ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.blogs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.social_links ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.contact_emails ENABLE ROW LEVEL SECURITY;
-- PUBLIC READ
CREATE POLICY "Public read" ON public.section_order FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.about_me FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.skill_categories FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.experiences FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.educations FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.languages FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.activities FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.certifications FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.certification_skills FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.projects FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.project_images FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.blogs FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.social_links FOR SELECT USING (true);
CREATE POLICY "Public read" ON public.contact_emails FOR SELECT USING (true);
-- ADMIN-ONLY WRITE (locked to site owner)
-- ⚠️ SETUP REQUIRED: Replace YOUR-USER-UUID-HERE with your Supabase Auth user ID.
CREATE POLICY "Admin insert" ON public.section_order FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.section_order FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.section_order FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.about_me FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.about_me FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.about_me FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.skill_categories FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.skill_categories FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.skill_categories FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.experiences FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.experiences FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.experiences FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.educations FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.educations FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.educations FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.languages FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.languages FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.languages FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.activities FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.activities FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.activities FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.certifications FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.certifications FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.certifications FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.certification_skills FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.certification_skills FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.certification_skills FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.projects FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.projects FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.projects FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.project_images FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.project_images FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.project_images FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.blogs FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.blogs FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.blogs FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.social_links FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.social_links FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.social_links FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin insert" ON public.contact_emails FOR INSERT WITH CHECK (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin update" ON public.contact_emails FOR UPDATE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
CREATE POLICY "Admin delete" ON public.contact_emails FOR DELETE USING (auth.uid() = 'YOUR-USER-UUID-HERE'::uuid);
-- =============================================
-- FUNCTIONS & TRIGGERS
-- =============================================
-- Atomic Reordering (Fix #14)
CREATE OR REPLACE FUNCTION reorder_items(
p_table text, p_ids uuid[], p_indices int[]
) RETURNS void AS $$
BEGIN
-- Basic table name validation to prevent SQL injection in dynamic query
IF p_table NOT IN ('projects', 'blogs', 'experiences', 'educations', 'skill_categories', 'languages', 'activities', 'certifications', 'project_images') THEN
RAISE EXCEPTION 'Invalid table name for reordering';
END IF;
FOR i IN 1..array_length(p_ids, 1) LOOP
EXECUTE format('UPDATE %I SET order_index = $1 WHERE id = $2', p_table)
USING p_indices[i], p_ids[i];
END LOOP;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Resource Creation Limits (Fix #16)
CREATE OR REPLACE FUNCTION enforce_resource_limits()
RETURNS trigger AS $$
BEGIN
IF TG_TABLE_NAME = 'projects' AND (SELECT count(*) FROM projects) >= 100 THEN
RAISE EXCEPTION 'Maximum projects limit reached (100)';
END IF;
IF TG_TABLE_NAME = 'blogs' AND (SELECT count(*) FROM blogs) >= 200 THEN
RAISE EXCEPTION 'Maximum blogs limit reached (200)';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER check_projects_limit
BEFORE INSERT ON projects
FOR EACH ROW EXECUTE FUNCTION enforce_resource_limits();
CREATE TRIGGER check_blogs_limit
BEFORE INSERT ON blogs
FOR EACH ROW EXECUTE FUNCTION enforce_resource_limits();