@nextblock-cms/db 0.2.10 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/package.json +13 -3
  2. package/supabase/config.toml +319 -0
  3. package/supabase/migrations/20250513194738_setup_roles_and_profiles.sql +41 -0
  4. package/supabase/migrations/20250513194910_auto_create_profile_trigger.sql +48 -0
  5. package/supabase/migrations/20250513194916_rls_for_profiles.sql +85 -0
  6. package/supabase/migrations/20250514125634_fix_recursive_rls_policies.sql +51 -0
  7. package/supabase/migrations/20250514143016_setup_languages_table.sql +66 -0
  8. package/supabase/migrations/20250514171549_create_pages_table.sql +73 -0
  9. package/supabase/migrations/20250514171550_create_posts_table.sql +61 -0
  10. package/supabase/migrations/20250514171552_create_media_table.sql +45 -0
  11. package/supabase/migrations/20250514171553_create_blocks_table.sql +54 -0
  12. package/supabase/migrations/20250514171615_create_navigation_table.sql +56 -0
  13. package/supabase/migrations/20250514171627_rls_policies_for_content_tables.sql +70 -0
  14. package/supabase/migrations/20250515194800_add_translation_group_id.sql +39 -0
  15. package/supabase/migrations/20250520171900_add_translation_group_to_nav_items.sql +21 -0
  16. package/supabase/migrations/20250521143933_seed_homepage_and_nav.sql +64 -0
  17. package/supabase/migrations/20250523145833_add_feature_image_to_posts.sql +8 -0
  18. package/supabase/migrations/20250523151737_add_rls_to_media_table.sql +18 -0
  19. package/supabase/migrations/20250526110400_add_image_dimensions_to_media.sql +14 -0
  20. package/supabase/migrations/20250526153321_optimize_rls_policies.sql +188 -0
  21. package/supabase/migrations/20250526172513_resolve_select_policy_overlaps.sql +96 -0
  22. package/supabase/migrations/20250526172853_resolve_remaining_rls_v5.sql +107 -0
  23. package/supabase/migrations/20250526173538_finalize_rls_cleanup_v7.sql +110 -0
  24. package/supabase/migrations/20250526174710_separate_write_policies_v8.sql +147 -0
  25. package/supabase/migrations/20250526175359_fix_languages_select_rls_v9.sql +81 -0
  26. package/supabase/migrations/20250526182940_fix_nav_read_policy_v10.sql +27 -0
  27. package/supabase/migrations/20250526183239_fix_posts_read_rls_v11.sql +59 -0
  28. package/supabase/migrations/20250526183746_fix_media_select_rls_v12.sql +39 -0
  29. package/supabase/migrations/20250526184205_consolidate_content_read_rls_v13.sql +61 -0
  30. package/supabase/migrations/20250526185854_optimize_indexes.sql +47 -0
  31. package/supabase/migrations/20250526190900_debug_blocks_rls.sql +56 -0
  32. package/supabase/migrations/20250526191217_consolidate_blocks_select_rls.sql +79 -0
  33. package/supabase/migrations/20250526192822_fix_handle_languages_update_search_path.sql +32 -0
  34. package/supabase/migrations/20250527150500_fix_blocks_rls_policy.sql +54 -0
  35. package/supabase/migrations/20250602150602_add_blur_data_url_to_media.sql +4 -0
  36. package/supabase/migrations/20250602150959_add_variants_to_media.sql +4 -0
  37. package/supabase/migrations/20250618124000_create_get_my_claim_function.sql +5 -0
  38. package/supabase/migrations/20250618124100_create_logos_table.sql +29 -0
  39. package/supabase/migrations/20250618130000_fix_linter_warnings.sql +58 -0
  40. package/supabase/migrations/20250618151500_revert_storage_rls.sql +6 -0
  41. package/supabase/migrations/20250619084800_reinstate_storage_rls.sql +13 -0
  42. package/supabase/migrations/20250619092430_widen_logo_insert_policy.sql +6 -0
  43. package/supabase/migrations/20250619093122_fix_get_my_claim_volatility.sql +5 -0
  44. package/supabase/migrations/20250619104249_consolidated_logo_rls_fix.sql +56 -0
  45. package/supabase/migrations/20250619110700_fix_logo_rls_again.sql +59 -0
  46. package/supabase/migrations/20250619113200_add_file_path_to_media.sql +4 -0
  47. package/supabase/migrations/20250619124100_fix_rls_performance_warnings.sql +74 -0
  48. package/supabase/migrations/20250619195500_create_site_settings_table.sql +28 -0
  49. package/supabase/migrations/20250619201500_add_anon_read_to_site_settings.sql +7 -0
  50. package/supabase/migrations/20250619202000_add_is_active_to_languages.sql +5 -0
  51. package/supabase/migrations/20250620085700_fix_site_settings_write_rls.sql +27 -0
  52. package/supabase/migrations/20250620095500_fix_profiles_read_rls.sql +11 -0
  53. package/supabase/migrations/20250620100000_use_security_definer_for_rls.sql +39 -0
  54. package/supabase/migrations/20250620130000_add_public_read_to_logos.sql +4 -0
  55. package/supabase/migrations/20250708091700_create_translations_table.sql +55 -0
  56. package/supabase/migrations/20250708093403_seed_translations_table.sql +20 -0
  57. package/supabase/migrations/20250708110600_fix_translations_rls_policies.sql +11 -0
  58. package/supabase/migrations/20250708112300_add_new_translations.sql +9 -0
  59. package/supabase/migrations/20250709120000_create_revisions_tables.sql +109 -0
  60. package/supabase/migrations/20251001113000_add_folder_to_media.sql +14 -0
  61. package/supabase/migrations/20251112113736_fix_search_path_functions.sql +74 -0
  62. package/supabase/migrations/20251112124444_fix_rls_performance.sql +63 -0
  63. package/supabase/migrations/20251112125935_fix_combined_policies.sql +194 -0
  64. package/supabase/migrations/20251112132146_fix_foreign_key_indexes.sql +21 -0
  65. package/supabase/migrations/20251112132525_cleanup_unused_indexes.sql +10 -0
  66. package/supabase/migrations/20251112132822_fix_final_indexes.sql +14 -0
  67. package/supabase/migrations/20251112140000_scaffold_foundational_content.sql +95 -0
  68. package/supabase/migrations/20251112141000_seed_homepage_blocks.sql +656 -0
  69. package/supabase/migrations/20251112142000_seed_how_it_works_post_blocks.sql +100 -0
  70. package/supabase/migrations/20251112143000_seed_additional_translations.sql +102 -0
  71. package/supabase/migrations/20251112145000_grant_public_schema_usage.sql +6 -0
  72. package/supabase/migrations/20251112145500_grant_select_on_public_tables.sql +19 -0
  73. package/supabase/migrations/20251117093000_add_admin_created_flag.sql +21 -0
  74. package/supabase/migrations/20251117103000_relax_profile_username_constraint.sql +6 -0
  75. package/supabase/migrations/20251117110000_relax_profiles_site_settings_rls_for_signup.sql +20 -0
  76. package/supabase/migrations/20251117112000_fix_handle_new_user_role_enum.sql +45 -0
  77. package/supabase/migrations/20251117113000_cleanup_rls_duplicates.sql +20 -0
  78. package/supabase/migrations/20251117200000_media_service_role_insert.sql +14 -0
  79. package/supabase/migrations/20251117201500_media_service_role_select.sql +11 -0
  80. package/supabase/migrations/20251117203000_media_admin_writer_select.sql +11 -0
  81. package/supabase/migrations/20251117204500_fix_media_permissions.sql +43 -0
  82. package/lib/supabase/client.d.ts +0 -9
  83. package/lib/supabase/middleware.d.ts +0 -2
  84. package/lib/supabase/server.d.ts +0 -7
  85. package/lib/supabase/ssg-client.d.ts +0 -2
  86. package/lib/supabase/types.d.ts +0 -635
@@ -0,0 +1,58 @@
1
+ -- Fix multiple permissive policies on logos table
2
+ DROP POLICY IF EXISTS "Allow admin users to manage logos" ON public.logos;
3
+ DROP POLICY IF EXISTS "Allow read access for authenticated users on logos" ON public.logos;
4
+
5
+ CREATE POLICY "Allow read access for authenticated users on logos"
6
+ ON public.logos
7
+ FOR SELECT
8
+ TO authenticated
9
+ USING (true);
10
+
11
+ CREATE POLICY "Allow admin users to insert logos"
12
+ ON public.logos
13
+ FOR INSERT TO authenticated
14
+ WITH CHECK ((get_my_claim('user_role'::text) = '"admin"'::jsonb));
15
+
16
+ CREATE POLICY "Allow admin users to update logos"
17
+ ON public.logos
18
+ FOR UPDATE TO authenticated
19
+ USING ((get_my_claim('user_role'::text) = '"admin"'::jsonb))
20
+ WITH CHECK ((get_my_claim('user_role'::text) = '"admin"'::jsonb));
21
+
22
+ CREATE POLICY "Allow admin users to delete logos"
23
+ ON public.logos
24
+ FOR DELETE TO authenticated
25
+ USING ((get_my_claim('user_role'::text) = '"admin"'::jsonb));
26
+
27
+ -- Fix mutable search path for get_my_claim
28
+ CREATE OR REPLACE FUNCTION get_my_claim(claim TEXT)
29
+ RETURNS JSONB AS $$
30
+ SET search_path = '';
31
+ SELECT COALESCE(current_setting('request.jwt.claims', true)::JSONB ->> claim, NULL)::JSONB
32
+ $$ LANGUAGE SQL STABLE;
33
+
34
+ -- Optimize RLS policies on blocks table
35
+ DROP POLICY IF EXISTS "blocks_are_readable_if_parent_is_published" ON public.blocks;
36
+ DROP POLICY IF EXISTS "admins_and_writers_can_manage_blocks" ON public.blocks;
37
+
38
+ CREATE POLICY "blocks_are_readable_if_parent_is_published"
39
+ ON public.blocks FOR SELECT
40
+ TO anon, authenticated
41
+ USING (
42
+ (page_id IS NOT NULL AND EXISTS (
43
+ SELECT 1
44
+ FROM public.pages p
45
+ WHERE p.id = blocks.page_id AND p.status = 'published'
46
+ )) OR
47
+ (post_id IS NOT NULL AND EXISTS (
48
+ SELECT 1
49
+ FROM public.posts pt
50
+ WHERE pt.id = blocks.post_id AND pt.status = 'published' AND (pt.published_at IS NULL OR pt.published_at <= now())
51
+ ))
52
+ );
53
+
54
+ CREATE POLICY "admins_and_writers_can_manage_blocks"
55
+ ON public.blocks FOR ALL
56
+ TO authenticated
57
+ USING ((SELECT get_my_claim('user_role'::text)) IN ('"admin"', '"writer"'))
58
+ WITH CHECK ((SELECT get_my_claim('user_role'::text)) IN ('"admin"', '"writer"'));
@@ -0,0 +1,6 @@
1
+ -- Reverts the RLS policies on storage.objects that caused the upload failure.
2
+
3
+ DROP POLICY IF EXISTS "allow_authenticated_uploads" ON storage.objects;
4
+ DROP POLICY IF EXISTS "allow_authenticated_select" ON storage.objects;
5
+ DROP POLICY IF EXISTS "allow_authenticated_updates" ON storage.objects;
6
+ DROP POLICY IF EXISTS "allow_authenticated_deletes" ON storage.objects;
@@ -0,0 +1,13 @@
1
+ -- Re-enables RLS policies for storage.objects to allow authenticated uploads.
2
+
3
+ -- Adds a policy allowing authenticated users to upload files
4
+ CREATE POLICY "allow_authenticated_uploads" ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'public' AND owner = auth.uid());
5
+
6
+ -- Allow authenticated users to SELECT files
7
+ CREATE POLICY "allow_authenticated_select" ON storage.objects FOR SELECT TO authenticated USING (bucket_id = 'public');
8
+
9
+ -- Allow authenticated users to UPDATE their own files
10
+ CREATE POLICY "allow_authenticated_updates" ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'public' AND owner = auth.uid());
11
+
12
+ -- Allow authenticated users to DELETE their own files
13
+ CREATE POLICY "allow_authenticated_deletes" ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'public' AND owner = auth.uid());
@@ -0,0 +1,6 @@
1
+ DROP POLICY IF EXISTS "Allow admins to manage logos" ON public.logos;
2
+
3
+ CREATE POLICY "Allow admin and writer users to insert logos"
4
+ ON public.logos
5
+ FOR INSERT TO authenticated
6
+ WITH CHECK ((get_my_claim('user_role'::text) IN ('"admin"'::jsonb, '"writer"'::jsonb)));
@@ -0,0 +1,5 @@
1
+ CREATE OR REPLACE FUNCTION get_my_claim(claim TEXT)
2
+ RETURNS JSONB AS $$
3
+ SET search_path = '';
4
+ SELECT COALESCE(current_setting('request.jwt.claims', true)::JSONB ->> claim, NULL)::JSONB
5
+ $$ LANGUAGE SQL VOLATILE;
@@ -0,0 +1,56 @@
1
+ -- Step 1: Drop all dependent policies first.
2
+ DROP POLICY IF EXISTS "Allow admins to manage logos" ON public.logos;
3
+ DROP POLICY IF EXISTS "Allow users to insert logos" ON public.logos;
4
+ DROP POLICY IF EXISTS "Allow logo insert for writers and admins" ON public.logos;
5
+ DROP POLICY IF EXISTS "Allow logo update for admins" ON public.logos;
6
+ DROP POLICY IF EXISTS "Allow logo delete for admins" ON public.logos;
7
+
8
+ -- Step 2: Drop the old function to avoid return type conflicts.
9
+ -- Note: It's critical to drop policies before the function they depend on.
10
+ DROP FUNCTION IF EXISTS get_my_claim(text) CASCADE;
11
+
12
+ -- Step 3: Redefine the get_my_claim function to be more robust and return TEXT.
13
+ -- This avoids JSON casting errors and type mismatches in policies.
14
+ CREATE OR REPLACE FUNCTION get_my_claim(claim TEXT)
15
+ RETURNS TEXT AS $$
16
+ DECLARE
17
+ claims jsonb;
18
+ claim_value text;
19
+ BEGIN
20
+ -- Safely get claims, defaulting to NULL if not present or invalid JSON
21
+ BEGIN
22
+ claims := current_setting('request.jwt.claims', true)::jsonb;
23
+ EXCEPTION
24
+ WHEN invalid_text_representation THEN
25
+ claims := NULL;
26
+ END;
27
+
28
+ -- If claims are NULL, return NULL
29
+ IF claims IS NULL THEN
30
+ RETURN NULL;
31
+ END IF;
32
+
33
+ -- Safely extract the claim value as text, removing quotes
34
+ claim_value := claims ->> claim;
35
+
36
+ RETURN claim_value;
37
+ END;
38
+ $$ LANGUAGE plpgsql VOLATILE;
39
+
40
+
41
+ -- Create the new, correct policies using the updated function
42
+ CREATE POLICY "Allow logo insert for authenticated users"
43
+ ON public.logos
44
+ FOR INSERT
45
+ WITH CHECK (auth.role() = 'authenticated');
46
+
47
+ CREATE POLICY "Allow logo update for authenticated users"
48
+ ON public.logos
49
+ FOR UPDATE
50
+ USING (auth.role() = 'authenticated')
51
+ WITH CHECK (auth.role() = 'authenticated');
52
+
53
+ CREATE POLICY "Allow logo delete for authenticated users"
54
+ ON public.logos
55
+ FOR DELETE
56
+ USING (auth.role() = 'authenticated');
@@ -0,0 +1,59 @@
1
+ -- Step 1: Drop all dependent policies first.
2
+ DROP POLICY IF EXISTS "Allow admins to manage logos" ON public.logos;
3
+ DROP POLICY IF EXISTS "Allow users to insert logos" ON public.logos;
4
+ DROP POLICY IF EXISTS "Allow logo insert for writers and admins" ON public.logos;
5
+ DROP POLICY IF EXISTS "Allow logo update for admins" ON public.logos;
6
+ DROP POLICY IF EXISTS "Allow logo delete for admins" ON public.logos;
7
+ DROP POLICY IF EXISTS "Allow logo insert for authenticated users" ON public.logos;
8
+ DROP POLICY IF EXISTS "Allow logo update for authenticated users" ON public.logos;
9
+ DROP POLICY IF EXISTS "Allow logo delete for authenticated users" ON public.logos;
10
+
11
+ -- Step 2: Drop the old function to avoid return type conflicts.
12
+ -- Note: It's critical to drop policies before the function they depend on.
13
+ DROP FUNCTION IF EXISTS get_my_claim(text) CASCADE;
14
+
15
+ -- Step 3: Redefine the get_my_claim function to be more robust and return TEXT.
16
+ -- This avoids JSON casting errors and type mismatches in policies.
17
+ CREATE OR REPLACE FUNCTION get_my_claim(claim TEXT)
18
+ RETURNS TEXT AS $$
19
+ DECLARE
20
+ claims jsonb;
21
+ claim_value text;
22
+ BEGIN
23
+ -- Safely get claims, defaulting to NULL if not present or invalid JSON
24
+ BEGIN
25
+ claims := current_setting('request.jwt.claims', true)::jsonb;
26
+ EXCEPTION
27
+ WHEN invalid_text_representation THEN
28
+ claims := NULL;
29
+ END;
30
+
31
+ -- If claims are NULL, return NULL
32
+ IF claims IS NULL THEN
33
+ RETURN NULL;
34
+ END IF;
35
+
36
+ -- Safely extract the claim value as text, removing quotes
37
+ claim_value := claims ->> claim;
38
+
39
+ RETURN claim_value;
40
+ END;
41
+ $$ LANGUAGE plpgsql VOLATILE;
42
+
43
+
44
+ -- Create the new, correct policies using the updated function
45
+ CREATE POLICY "Allow logo insert for authenticated users"
46
+ ON public.logos
47
+ FOR INSERT
48
+ WITH CHECK (auth.role() = 'authenticated');
49
+
50
+ CREATE POLICY "Allow logo update for authenticated users"
51
+ ON public.logos
52
+ FOR UPDATE
53
+ USING (auth.role() = 'authenticated')
54
+ WITH CHECK (auth.role() = 'authenticated');
55
+
56
+ CREATE POLICY "Allow logo delete for authenticated users"
57
+ ON public.logos
58
+ FOR DELETE
59
+ USING (auth.role() = 'authenticated');
@@ -0,0 +1,4 @@
1
+ ALTER TABLE public.media
2
+ ADD COLUMN file_path TEXT;
3
+
4
+ COMMENT ON COLUMN public.media.file_path IS 'The full path to the file in the storage bucket.';
@@ -0,0 +1,74 @@
1
+ BEGIN;
2
+
3
+ -- Drop existing policies for 'public.logos'
4
+ DROP POLICY IF EXISTS "Allow logo insert for authenticated users" ON public.logos;
5
+ DROP POLICY IF EXISTS "Allow logo update for authenticated users" ON public.logos;
6
+ DROP POLICY IF EXISTS "Allow logo delete for authenticated users" ON public.logos;
7
+
8
+ -- Recreate policies for 'public.logos' with optimized auth calls
9
+ CREATE POLICY "Allow logo insert for authenticated users"
10
+ ON public.logos
11
+ FOR INSERT
12
+ WITH CHECK ((SELECT auth.role()) = 'authenticated');
13
+
14
+ CREATE POLICY "Allow logo update for authenticated users"
15
+ ON public.logos
16
+ FOR UPDATE
17
+ USING ((SELECT auth.role()) = 'authenticated')
18
+ WITH CHECK ((SELECT auth.role()) = 'authenticated');
19
+
20
+ CREATE POLICY "Allow logo delete for authenticated users"
21
+ ON public.logos
22
+ FOR DELETE
23
+ USING ((SELECT auth.role()) = 'authenticated');
24
+
25
+ -- Drop existing policies for 'public.blocks'
26
+ DROP POLICY IF EXISTS "blocks_authenticated_comprehensive_select" ON public.blocks;
27
+ DROP POLICY IF EXISTS "blocks_admin_writer_can_insert" ON public.blocks;
28
+ DROP POLICY IF EXISTS "blocks_admin_writer_can_update" ON public.blocks;
29
+ DROP POLICY IF EXISTS "blocks_admin_writer_can_delete" ON public.blocks;
30
+ DROP POLICY IF EXISTS "blocks_anon_can_read_published_blocks" ON public.blocks;
31
+ DROP POLICY IF EXISTS "blocks_are_readable_if_parent_is_published" ON public.blocks;
32
+
33
+ -- Create a new comprehensive SELECT policy for 'public.blocks'
34
+ CREATE POLICY "Allow read access to blocks" ON public.blocks
35
+ FOR SELECT USING (
36
+ (
37
+ -- Anonymous users can read blocks of published content
38
+ (SELECT auth.role()) = 'anon' AND
39
+ (
40
+ (page_id IS NOT NULL AND EXISTS(SELECT 1 FROM public.pages p WHERE p.id = blocks.page_id AND p.status = 'published')) OR
41
+ (post_id IS NOT NULL AND EXISTS(SELECT 1 FROM public.posts pt WHERE pt.id = blocks.post_id AND pt.status = 'published' AND (pt.published_at IS NULL OR pt.published_at <= now())))
42
+ )
43
+ ) OR (
44
+ -- Authenticated users have role-based access
45
+ (SELECT auth.role()) = 'authenticated' AND
46
+ (
47
+ (
48
+ -- ADMIN or WRITER can read all blocks
49
+ EXISTS (SELECT 1 FROM public.profiles WHERE id = (SELECT auth.uid()) AND role IN ('ADMIN', 'WRITER'))
50
+ ) OR
51
+ (
52
+ -- USER can read blocks of published parents
53
+ EXISTS (SELECT 1 FROM public.profiles WHERE id = (SELECT auth.uid()) AND role = 'USER') AND
54
+ (
55
+ (page_id IS NOT NULL AND EXISTS(SELECT 1 FROM public.pages p WHERE p.id = blocks.page_id AND p.status = 'published')) OR
56
+ (post_id IS NOT NULL AND EXISTS(SELECT 1 FROM public.posts pt WHERE pt.id = blocks.post_id AND pt.status = 'published' AND (pt.published_at IS NULL OR pt.published_at <= now())))
57
+ )
58
+ )
59
+ )
60
+ )
61
+ );
62
+
63
+ -- Re-create the management policies for 'public.blocks' with optimized auth calls
64
+ CREATE POLICY "Allow insert for admins and writers on blocks" ON public.blocks
65
+ FOR INSERT WITH CHECK (EXISTS (SELECT 1 FROM public.profiles WHERE id = (SELECT auth.uid()) AND role IN ('ADMIN', 'WRITER')));
66
+
67
+ CREATE POLICY "Allow update for admins and writers on blocks" ON public.blocks
68
+ FOR UPDATE USING (EXISTS (SELECT 1 FROM public.profiles WHERE id = (SELECT auth.uid()) AND role IN ('ADMIN', 'WRITER')))
69
+ WITH CHECK (EXISTS (SELECT 1 FROM public.profiles WHERE id = (SELECT auth.uid()) AND role IN ('ADMIN', 'WRITER')));
70
+
71
+ CREATE POLICY "Allow delete for admins and writers on blocks" ON public.blocks
72
+ FOR DELETE USING (EXISTS (SELECT 1 FROM public.profiles WHERE id = (SELECT auth.uid()) AND role IN ('ADMIN', 'WRITER')));
73
+
74
+ COMMIT;
@@ -0,0 +1,28 @@
1
+ -- supabase/migrations/20250619195500_create_site_settings_table.sql
2
+
3
+ CREATE TABLE public.site_settings (
4
+ key TEXT PRIMARY KEY,
5
+ value JSONB
6
+ );
7
+
8
+ -- Enable RLS
9
+ ALTER TABLE public.site_settings ENABLE ROW LEVEL SECURITY;
10
+
11
+ -- Allow admins to do everything
12
+ CREATE POLICY "Allow admins full access on site_settings"
13
+ ON public.site_settings
14
+ FOR ALL
15
+ TO authenticated
16
+ USING (get_my_claim('user_role') = '"admin"');
17
+
18
+ -- Allow authenticated users to read settings
19
+ CREATE POLICY "Allow authenticated users to read site_settings"
20
+ ON public.site_settings
21
+ FOR SELECT
22
+ TO authenticated
23
+ USING (true);
24
+
25
+ -- Seed initial copyright setting
26
+ INSERT INTO public.site_settings (key, value)
27
+ VALUES ('footer_copyright', '{"en": "© {year} Nextblock CMS. All rights reserved.", "fr": "© {year} Nextblock CMS. Tous droits réservés."}')
28
+ ON CONFLICT (key) DO NOTHING;
@@ -0,0 +1,7 @@
1
+ -- supabase/migrations/20250619201500_add_anon_read_to_site_settings.sql
2
+
3
+ CREATE POLICY "Allow public read access to site_settings"
4
+ ON public.site_settings
5
+ FOR SELECT
6
+ TO anon
7
+ USING (true);
@@ -0,0 +1,5 @@
1
+ -- Add is_active column to languages table
2
+ ALTER TABLE public.languages
3
+ ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true;
4
+
5
+ COMMENT ON COLUMN public.languages.is_active IS 'Indicates if the language is currently active and available for use.';
@@ -0,0 +1,27 @@
1
+ -- Drop existing policies if they exist to avoid conflicts.
2
+ DROP POLICY IF EXISTS "Allow ADMIN and WRITER to insert into site_settings" ON public.site_settings;
3
+ DROP POLICY IF EXISTS "Allow ADMIN and WRITER to update site_settings" ON public.site_settings;
4
+ DROP POLICY IF EXISTS "Allow ADMIN and WRITER to modify site_settings" ON public.site_settings;
5
+
6
+ -- This policy grants permission to insert into the site_settings table
7
+ -- to any authenticated user whose role in the profiles table is 'ADMIN' or 'WRITER'.
8
+ CREATE POLICY "Allow ADMIN and WRITER to insert into site_settings"
9
+ ON public.site_settings
10
+ FOR INSERT
11
+ TO authenticated
12
+ WITH CHECK (
13
+ (SELECT role FROM public.profiles WHERE id = auth.uid()) IN ('ADMIN', 'WRITER')
14
+ );
15
+
16
+ -- This policy grants permission to update the site_settings table
17
+ -- to any authenticated user whose role in the profiles table is 'ADMIN' or 'WRITER'.
18
+ CREATE POLICY "Allow ADMIN and WRITER to update site_settings"
19
+ ON public.site_settings
20
+ FOR UPDATE
21
+ TO authenticated
22
+ USING (
23
+ (SELECT role FROM public.profiles WHERE id = auth.uid()) IN ('ADMIN', 'WRITER')
24
+ )
25
+ WITH CHECK (
26
+ (SELECT role FROM public.profiles WHERE id = auth.uid()) IN ('ADMIN', 'WRITER')
27
+ );
@@ -0,0 +1,11 @@
1
+ -- Drop the policy if it exists to ensure a clean state.
2
+ DROP POLICY IF EXISTS "Allow user to read their own profile" ON public.profiles;
3
+
4
+ -- This policy allows an authenticated user to read their own row from the profiles table.
5
+ -- This is necessary for other RLS policies (like the one on site_settings) to be able
6
+ -- to look up the user's role during policy evaluation.
7
+ CREATE POLICY "Allow user to read their own profile"
8
+ ON public.profiles
9
+ FOR SELECT
10
+ TO authenticated
11
+ USING (auth.uid() = id);
@@ -0,0 +1,39 @@
1
+ -- Drop all previous policies on site_settings to ensure a clean slate.
2
+ DROP POLICY IF EXISTS "Allow ADMIN and WRITER to insert into site_settings" ON public.site_settings;
3
+ DROP POLICY IF EXISTS "Allow ADMIN and WRITER to update site_settings" ON public.site_settings;
4
+ DROP POLICY IF EXISTS "Allow ADMIN and WRITER to modify site_settings" ON public.site_settings;
5
+
6
+ -- Create a trusted, elevated-privilege function to get the current user's role.
7
+ -- SECURITY DEFINER makes it run with the permissions of the function owner, bypassing nested RLS.
8
+ CREATE OR REPLACE FUNCTION get_my_role()
9
+ RETURNS TEXT AS $$
10
+ DECLARE
11
+ user_role TEXT;
12
+ BEGIN
13
+ SELECT role INTO user_role FROM public.profiles WHERE id = auth.uid();
14
+ RETURN user_role;
15
+ END;
16
+ $$ LANGUAGE plpgsql SECURITY DEFINER;
17
+
18
+ -- This policy grants permission to insert into the site_settings table
19
+ -- by using the trusted function to check the user's role.
20
+ CREATE POLICY "Allow insert based on user role"
21
+ ON public.site_settings
22
+ FOR INSERT
23
+ TO authenticated
24
+ WITH CHECK (
25
+ get_my_role() IN ('ADMIN', 'WRITER')
26
+ );
27
+
28
+ -- This policy grants permission to update the site_settings table
29
+ -- by using the trusted function to check the user's role.
30
+ CREATE POLICY "Allow update based on user role"
31
+ ON public.site_settings
32
+ FOR UPDATE
33
+ TO authenticated
34
+ USING (
35
+ get_my_role() IN ('ADMIN', 'WRITER')
36
+ )
37
+ WITH CHECK (
38
+ get_my_role() IN ('ADMIN', 'WRITER')
39
+ );
@@ -0,0 +1,4 @@
1
+ CREATE POLICY "Allow public read access to logos"
2
+ ON public.logos
3
+ FOR SELECT
4
+ USING (true);
@@ -0,0 +1,55 @@
1
+ -- Create the translations table
2
+ CREATE TABLE translations (
3
+ key TEXT PRIMARY KEY,
4
+ translations JSONB NOT NULL,
5
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
6
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
7
+ );
8
+
9
+ -- Add comments on the columns
10
+ COMMENT ON COLUMN translations.key IS 'A unique, slugified identifier (e.g., "sign_in_button_text").';
11
+ COMMENT ON COLUMN translations.translations IS 'Stores translations as key-value pairs (e.g., {"en": "Sign In", "fr": "s''inscrire"}).';
12
+
13
+ -- Enable Row Level Security
14
+ ALTER TABLE translations ENABLE ROW LEVEL SECURITY;
15
+
16
+ -- RLS Policies
17
+ CREATE POLICY "Allow all access to ADMIN"
18
+ ON public.translations
19
+ FOR ALL
20
+ TO authenticated
21
+ USING (
22
+ (auth.jwt() ->> 'user_role') = 'ADMIN'
23
+ )
24
+ WITH CHECK (
25
+ (auth.jwt() ->> 'user_role') = 'ADMIN'
26
+ );
27
+
28
+ CREATE POLICY "Allow read access to all authenticated users"
29
+ ON public.translations
30
+ FOR SELECT
31
+ TO authenticated
32
+ USING (true);
33
+
34
+ CREATE POLICY "Allow read access to all anonymous users"
35
+ ON public.translations
36
+ FOR SELECT
37
+ TO anon
38
+ USING (true);
39
+
40
+ -- Trigger to update updated_at timestamp
41
+ CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at()
42
+ RETURNS TRIGGER AS $$
43
+ DECLARE
44
+ _new record;
45
+ BEGIN
46
+ _new := NEW;
47
+ _new."updated_at" = NOW();
48
+ RETURN _new;
49
+ END;
50
+ $$ LANGUAGE plpgsql;
51
+
52
+ CREATE TRIGGER set_updated_at
53
+ BEFORE UPDATE ON public.translations
54
+ FOR EACH ROW
55
+ EXECUTE FUNCTION public.set_current_timestamp_updated_at();
@@ -0,0 +1,20 @@
1
+ INSERT INTO public.translations (key, translations) VALUES
2
+ ('sign_in', '{"en": "Sign in", "fr": "Connexion"}'),
3
+ ('sign_up', '{"en": "Sign up", "fr": "Inscription"}'),
4
+ ('sign_out', '{"en": "Sign out", "fr": "Déconnexion"}'),
5
+ ('dont_have_account', '{"en": "Don''t have an account?", "fr": "Pas encore de compte ?"}'),
6
+ ('email', '{"en": "Email", "fr": "Email"}'),
7
+ ('you_at_example_com', '{"en": "you@example.com", "fr": "vous@example.com"}'),
8
+ ('password', '{"en": "Password", "fr": "Mot de passe"}'),
9
+ ('forgot_password', '{"en": "Forgot Password?", "fr": "Mot de passe oublié ?"}'),
10
+ ('your_password', '{"en": "Your password", "fr": "Votre mot de passe"}'),
11
+ ('signing_in_pending', '{"en": "Signing In...", "fr": "Connexion en cours..."}'),
12
+ ('already_have_account', '{"en": "Already have an account?", "fr": "Déjà un compte ?"}'),
13
+ ('signing_up_pending', '{"en": "Signing up...", "fr": "Inscription en cours..."}'),
14
+ ('reset_password', '{"en": "Reset Password", "fr": "Réinitialiser le mot de passe"}');
15
+
16
+ -- Route prefix for the blog section
17
+ INSERT INTO public.translations (key, translations) VALUES
18
+ ('blog_prefix', '{"en": "article", "fr": "article"}')
19
+ ON CONFLICT (key) DO UPDATE
20
+ SET translations = EXCLUDED.translations;
@@ -0,0 +1,11 @@
1
+ -- Drop the existing restrictive policy for updates
2
+ DROP POLICY IF EXISTS "Allow all access to ADMIN" ON public.translations;
3
+
4
+ -- Create a more permissive policy that allows authenticated users to update translations
5
+ -- This assumes that access to the CMS is already controlled at the application level
6
+ CREATE POLICY "Allow authenticated users to manage translations"
7
+ ON public.translations
8
+ FOR ALL
9
+ TO authenticated
10
+ USING (true)
11
+ WITH CHECK (true);
@@ -0,0 +1,9 @@
1
+ INSERT INTO public.translations (key, translations) VALUES
2
+ ('edit_page', '{"en": "Edit Page", "fr": "Éditer la page"}'),
3
+ ('edit_post', '{"en": "Edit Post", "fr": "Éditer l''article"}'),
4
+ ('open_main_menu', '{"en": "Open main menu", "fr": "Ouvrir le menu principal"}'),
5
+ ('mobile_navigation_menu', '{"en": "Mobile navigation menu", "fr": "Menu de navigation mobile"}'),
6
+ ('cms_dashboard', '{"en": "CMS Dashboard", "fr": "Tableau de bord CMS"}'),
7
+ ('update_env_file_warning', '{"en": "Please update .env.local file with anon key and url", "fr": "Veuillez mettre à jour .env.local avec l''anon key et l''URL"}'),
8
+ ('greeting', '{"en": "Hey, {username}!", "fr": "Salut, {username} !"}')
9
+ ON CONFLICT (key) DO NOTHING;
@@ -0,0 +1,109 @@
1
+ -- supabase/migrations/20250709120000_create_revisions_tables.sql
2
+ -- Hybrid revision history for pages and posts
3
+
4
+ begin;
5
+
6
+ -- Create enum for revision type if not exists
7
+ do $$
8
+ begin
9
+ if not exists (select 1 from pg_type where typname = 'revision_type') then
10
+ create type public.revision_type as enum ('snapshot', 'diff');
11
+ end if;
12
+ end
13
+ $$;
14
+
15
+ -- Add version column to pages and posts if not exists
16
+ do $$
17
+ begin
18
+ if not exists (
19
+ select 1 from information_schema.columns
20
+ where table_schema = 'public' and table_name = 'pages' and column_name = 'version'
21
+ ) then
22
+ alter table public.pages add column version integer not null default 1;
23
+ comment on column public.pages.version is 'Monotonic version number for hybrid revisions.';
24
+ end if;
25
+ end
26
+ $$;
27
+
28
+ do $$
29
+ begin
30
+ if not exists (
31
+ select 1 from information_schema.columns
32
+ where table_schema = 'public' and table_name = 'posts' and column_name = 'version'
33
+ ) then
34
+ alter table public.posts add column version integer not null default 1;
35
+ comment on column public.posts.version is 'Monotonic version number for hybrid revisions.';
36
+ end if;
37
+ end
38
+ $$;
39
+
40
+ -- Create page_revisions table
41
+ create table if not exists public.page_revisions (
42
+ id bigint generated by default as identity primary key,
43
+ page_id bigint not null references public.pages(id) on delete cascade,
44
+ author_id uuid references public.profiles(id) on delete set null,
45
+ version integer not null,
46
+ revision_type public.revision_type not null,
47
+ content jsonb not null,
48
+ created_at timestamp with time zone not null default now(),
49
+ constraint page_revisions_page_version_key unique (page_id, version)
50
+ );
51
+
52
+ comment on table public.page_revisions is 'Hybrid (snapshot/diff) revisions for pages.';
53
+ comment on column public.page_revisions.content is 'If snapshot: full content; if diff: JSON Patch array.';
54
+
55
+ create index if not exists idx_page_revisions_page_id on public.page_revisions(page_id);
56
+ create index if not exists idx_page_revisions_page_id_version on public.page_revisions(page_id, version);
57
+
58
+ -- Create post_revisions table
59
+ create table if not exists public.post_revisions (
60
+ id bigint generated by default as identity primary key,
61
+ post_id bigint not null references public.posts(id) on delete cascade,
62
+ author_id uuid references public.profiles(id) on delete set null,
63
+ version integer not null,
64
+ revision_type public.revision_type not null,
65
+ content jsonb not null,
66
+ created_at timestamp with time zone not null default now(),
67
+ constraint post_revisions_post_version_key unique (post_id, version)
68
+ );
69
+
70
+ comment on table public.post_revisions is 'Hybrid (snapshot/diff) revisions for posts.';
71
+ comment on column public.post_revisions.content is 'If snapshot: full content; if diff: JSON Patch array.';
72
+
73
+ create index if not exists idx_post_revisions_post_id on public.post_revisions(post_id);
74
+ create index if not exists idx_post_revisions_post_id_version on public.post_revisions(post_id, version);
75
+
76
+ -- Enable RLS and add policies (admins and writers manage; authenticated can read)
77
+ alter table public.page_revisions enable row level security;
78
+ alter table public.post_revisions enable row level security;
79
+
80
+ -- Page revisions policies
81
+ drop policy if exists "page_revisions_admin_writer_management" on public.page_revisions;
82
+ create policy "page_revisions_admin_writer_management"
83
+ on public.page_revisions for all
84
+ to authenticated
85
+ using (public.get_current_user_role() in ('ADMIN', 'WRITER'))
86
+ with check (public.get_current_user_role() in ('ADMIN', 'WRITER'));
87
+
88
+ drop policy if exists "page_revisions_authenticated_read" on public.page_revisions;
89
+ create policy "page_revisions_authenticated_read"
90
+ on public.page_revisions for select
91
+ to authenticated
92
+ using (true);
93
+
94
+ -- Post revisions policies
95
+ drop policy if exists "post_revisions_admin_writer_management" on public.post_revisions;
96
+ create policy "post_revisions_admin_writer_management"
97
+ on public.post_revisions for all
98
+ to authenticated
99
+ using (public.get_current_user_role() in ('ADMIN', 'WRITER'))
100
+ with check (public.get_current_user_role() in ('ADMIN', 'WRITER'));
101
+
102
+ drop policy if exists "post_revisions_authenticated_read" on public.post_revisions;
103
+ create policy "post_revisions_authenticated_read"
104
+ on public.post_revisions for select
105
+ to authenticated
106
+ using (true);
107
+
108
+ commit;
109
+
@@ -0,0 +1,14 @@
1
+ -- Add a folder column to organize media by path prefix
2
+ ALTER TABLE public.media
3
+ ADD COLUMN IF NOT EXISTS folder TEXT;
4
+
5
+ COMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object (e.g., images/summer/).';
6
+
7
+ -- Backfill existing rows by deriving folder from object_key (everything up to last slash)
8
+ UPDATE public.media
9
+ SET folder = NULLIF(regexp_replace(object_key, '[^/]*$', ''), '')
10
+ WHERE folder IS NULL;
11
+
12
+ -- Index to speed up filtering by folder
13
+ CREATE INDEX IF NOT EXISTS media_folder_idx ON public.media (folder);
14
+