@bygghammar/cms-platform 0.11.0 → 0.12.0

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 (63) hide show
  1. package/migrations/20260728113741_f9744b19-078b-46e5-b00f-31c84aaf71ab.sql +111 -0
  2. package/migrations/20260728113757_4156a886-21fb-4a1f-9c12-acb7bb145102.sql +5 -0
  3. package/migrations/20260728113823_6e0237e1-99e3-4e1a-8a68-804b7bcdbbb8.sql +1 -0
  4. package/migrations/20260728115305_506d92b4-129f-4d2b-961f-cf76e592677e.sql +92 -0
  5. package/migrations/20260728115320_717d4447-f433-4a1d-89d2-fc8f028d4ac6.sql +1 -0
  6. package/migrations/20260728115347_c77f5a56-19db-4117-bb60-9ad692b817ab.sql +11 -0
  7. package/migrations/20260728132052_b692dcfe-a944-4a19-afd0-187f7600feb8.sql +63 -0
  8. package/migrations/20260728183318_05b05a47-ba05-4a66-ab8f-69f2688fea84.sql +209 -0
  9. package/migrations/20260728193035_84b68d27-c3f3-4516-a3a9-15fc6f476163.sql +17 -0
  10. package/migrations/20260728193707_8438ee2d-d589-4048-86e2-464fd209af80.sql +37 -0
  11. package/migrations/20260729055312_08862d7e-0279-4d4f-8709-a59b9f068c55.sql +5 -0
  12. package/migrations/20260729055348_418b7e36-4877-4304-831f-290ca73e56f3.sql +133 -0
  13. package/migrations/20260729055358_20b467e2-4efd-42c1-bd34-773ed6b77fc5.sql +3 -0
  14. package/migrations/20260729061039_306859eb-a38c-44ef-af1a-6c02a8fd84db.sql +43 -0
  15. package/migrations/20260730065600_54c25861-3355-4600-a72f-3d67b457ecb1.sql +54 -0
  16. package/migrations/20260730071745_d4904314-15ff-4c58-ac66-85e7371e04b5.sql +113 -0
  17. package/migrations/20260730071914_48163aa3-830e-4dcc-bdd4-c35be282ab87.sql +22 -0
  18. package/migrations/20260730085949_bcfc3270-ea80-4617-86a3-2555ed34a280.sql +24 -0
  19. package/migrations/20260730095256_c977c30e-b285-4041-8804-c0bd72b6d9b3.sql +13 -0
  20. package/migrations/20260730100057_e341b887-eb0e-4a74-850e-0ee5ffb70d7a.sql +38 -0
  21. package/migrations/20260730103703_d5c4e1b7-1e2c-4dd0-be4e-185c159ade69.sql +62 -0
  22. package/migrations/20260730105635_3557b7da-919e-48db-8c92-8221d68d3cf9.sql +29 -0
  23. package/migrations/20260730120308_12ff8cfb-43f9-4070-abde-61531ad3b75f.sql +73 -0
  24. package/migrations/20260730125024_1baf8926-12a2-48f4-8edc-7309941309e9.sql +18 -0
  25. package/migrations/20260730173510_feb974d1-510e-4f52-9e20-5fd0582d8952.sql +107 -0
  26. package/migrations/20260730175409_9f60743f-f5e1-418c-a700-c91fb3704db8.sql +9 -0
  27. package/migrations/20260730181224_1c52ff16-c7a2-4ef1-a0a8-e493c2c9106f.sql +7 -0
  28. package/migrations/20260730183129_933adb75-f985-4d82-ad5d-213b69fd42e5.sql +32 -0
  29. package/migrations/20260730183918_7bc7a827-2100-49f1-bc6e-24509249350b.sql +68 -0
  30. package/migrations/20260730202628_7ceade5e-644c-4bc2-91b8-594133329f31.sql +1 -0
  31. package/migrations/20260730211056_57f42049-00b4-473b-a2cd-65a13570b8d2.sql +16 -0
  32. package/migrations/20260730214340_98af2e55-9031-4c11-9659-c47a37a8d6b7.sql +1 -0
  33. package/migrations/20260731091630_8a6f28ce-5833-4901-ab17-27648cef32ec.sql +149 -0
  34. package/migrations/20260731094716_4c68a1df-83ed-4ba8-8019-fa2fe2b9681e.sql +65 -0
  35. package/migrations/20260731191655_fef97c3c-1397-47b4-8b56-afb020298e97.sql +165 -0
  36. package/migrations/20260731192212_95772a2f-19a1-4013-b463-2eb161e96737.sql +135 -0
  37. package/migrations/20260731192815_c490653e-1232-4ad9-a150-3c83d2b90f5e.sql +13 -0
  38. package/migrations/20260731193135_7966e740-5e86-4b14-8e1d-5119069666ba.sql +2 -0
  39. package/migrations/20260731193245_599790d0-ca5e-4b5d-a3e7-d69b2a79e1a3.sql +13 -0
  40. package/migrations/20260731212959_a748cb0d-4dc7-45dc-be90-f6bacbfab26c.sql +9 -0
  41. package/migrations/20260801003057_1195f4c1-fb54-42dc-9718-638a25e6a82e.sql +3 -0
  42. package/migrations/20260801003300_fc591960-cb41-41cf-837d-2eef23b29ca9.sql +40 -0
  43. package/migrations/20260802151445_a56b0023-eaf7-41e2-a7fb-d4beddfc587c.sql +3 -0
  44. package/migrations/20260802202621_d661af8a-8513-4b54-b375-f963ae824c8f.sql +225 -0
  45. package/migrations/20260802202702_c30cd837-744c-4641-85f1-edd3a7ebbcb6.sql +16 -0
  46. package/migrations/20260802202720_4418d18d-c851-42dc-9345-78fc330128ae.sql +2 -0
  47. package/migrations/20260803132514_78ba1f53-c85b-4457-96b1-7d49c5c8b494.sql +34 -0
  48. package/migrations/20260803181201_28c8aa09-baa8-431c-8338-90b1af9c8172.sql +22 -0
  49. package/migrations/20260803201518_6e8a6028-b70f-4a7d-8bc9-95c86304e7ae.sql +12 -0
  50. package/migrations/20260803201535_32a9a213-cb92-4035-b8cd-8211ca34af68.sql +12 -0
  51. package/migrations/20260811103505_29df753d-14fa-4a05-8fea-f4207785fba3.sql +35 -0
  52. package/migrations/20260811112455_217c4028-f587-4c98-9e4f-9e6c452ab761.sql +20 -0
  53. package/migrations/20260811114438_6687d67d-bad3-4803-9638-8b7670d2e5d4.sql +86 -0
  54. package/migrations/20260811130318_7cb95e95-9baa-40af-8f3f-de1d4e09cec4.sql +1 -0
  55. package/migrations/20260811220548_80d99ec2-bd4b-475d-a627-9408bb60ce56.sql +1 -0
  56. package/migrations/20260811222519_6b040f56-4de3-49e0-92e4-d9deec5ead6a.sql +3 -0
  57. package/migrations/20260812000000_seed_voyara_content.sql +390 -0
  58. package/migrations/20260818040722_1bdf1b8f-7fb0-4ae8-8ecf-73f1fadff3c2.sql +2 -0
  59. package/migrations/20260818040808_69a5e832-57e8-4aa7-aff8-7fd7777faa7c.sql +15 -0
  60. package/migrations/20260818040839_e6523163-bbf5-4a39-9233-91759059569a.sql +26 -0
  61. package/package.json +8 -4
  62. package/src/backoffice/media/ReviewImageUpload.tsx +104 -0
  63. package/src/platform/contract.ts +92 -0
@@ -0,0 +1,111 @@
1
+
2
+ CREATE TYPE public.app_role AS ENUM ('admin', 'editor');
3
+
4
+ CREATE TABLE public.user_roles (
5
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
7
+ role public.app_role NOT NULL,
8
+ created_at timestamptz NOT NULL DEFAULT now(),
9
+ UNIQUE (user_id, role)
10
+ );
11
+ GRANT SELECT ON public.user_roles TO authenticated;
12
+ GRANT ALL ON public.user_roles TO service_role;
13
+ ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY;
14
+
15
+ CREATE POLICY "Users can view their own roles"
16
+ ON public.user_roles
17
+ FOR SELECT
18
+ TO authenticated
19
+ USING (auth.uid() = user_id);
20
+
21
+ CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role public.app_role)
22
+ RETURNS boolean
23
+ LANGUAGE sql
24
+ STABLE
25
+ SECURITY DEFINER
26
+ SET search_path = public
27
+ AS $$
28
+ SELECT EXISTS (
29
+ SELECT 1 FROM public.user_roles
30
+ WHERE user_id = _user_id AND role = _role
31
+ )
32
+ $$;
33
+
34
+ CREATE TABLE public.content_sections (
35
+ key text PRIMARY KEY,
36
+ data jsonb NOT NULL DEFAULT '{}'::jsonb,
37
+ updated_at timestamptz NOT NULL DEFAULT now(),
38
+ updated_by uuid REFERENCES auth.users(id) ON DELETE SET NULL
39
+ );
40
+ GRANT SELECT ON public.content_sections TO anon, authenticated;
41
+ GRANT INSERT, UPDATE, DELETE ON public.content_sections TO authenticated;
42
+ GRANT ALL ON public.content_sections TO service_role;
43
+ ALTER TABLE public.content_sections ENABLE ROW LEVEL SECURITY;
44
+
45
+ CREATE POLICY "Anyone can read content sections"
46
+ ON public.content_sections
47
+ FOR SELECT
48
+ TO anon, authenticated
49
+ USING (true);
50
+
51
+ CREATE POLICY "Admins can insert content sections"
52
+ ON public.content_sections
53
+ FOR INSERT
54
+ TO authenticated
55
+ WITH CHECK (public.has_role(auth.uid(), 'admin'));
56
+
57
+ CREATE POLICY "Admins can update content sections"
58
+ ON public.content_sections
59
+ FOR UPDATE
60
+ TO authenticated
61
+ USING (public.has_role(auth.uid(), 'admin'))
62
+ WITH CHECK (public.has_role(auth.uid(), 'admin'));
63
+
64
+ CREATE POLICY "Admins can delete content sections"
65
+ ON public.content_sections
66
+ FOR DELETE
67
+ TO authenticated
68
+ USING (public.has_role(auth.uid(), 'admin'));
69
+
70
+ CREATE OR REPLACE FUNCTION public.touch_content_sections()
71
+ RETURNS trigger
72
+ LANGUAGE plpgsql
73
+ SET search_path = public
74
+ AS $$
75
+ BEGIN
76
+ NEW.updated_at = now();
77
+ NEW.updated_by = auth.uid();
78
+ RETURN NEW;
79
+ END;
80
+ $$;
81
+
82
+ CREATE TRIGGER content_sections_touch
83
+ BEFORE INSERT OR UPDATE ON public.content_sections
84
+ FOR EACH ROW EXECUTE FUNCTION public.touch_content_sections();
85
+
86
+ CREATE OR REPLACE FUNCTION public.grant_initial_admin()
87
+ RETURNS trigger
88
+ LANGUAGE plpgsql
89
+ SECURITY DEFINER
90
+ SET search_path = public
91
+ AS $$
92
+ BEGIN
93
+ IF NEW.email_confirmed_at IS NOT NULL
94
+ AND lower(NEW.email) = 'byggis@gmail.com' THEN
95
+ INSERT INTO public.user_roles (user_id, role)
96
+ VALUES (NEW.id, 'admin')
97
+ ON CONFLICT (user_id, role) DO NOTHING;
98
+ END IF;
99
+ RETURN NEW;
100
+ END;
101
+ $$;
102
+
103
+ CREATE TRIGGER on_auth_user_created_grant_admin
104
+ AFTER INSERT ON auth.users
105
+ FOR EACH ROW EXECUTE FUNCTION public.grant_initial_admin();
106
+
107
+ CREATE TRIGGER on_auth_user_confirmed_grant_admin
108
+ AFTER UPDATE OF email_confirmed_at ON auth.users
109
+ FOR EACH ROW
110
+ WHEN (OLD.email_confirmed_at IS NULL AND NEW.email_confirmed_at IS NOT NULL)
111
+ EXECUTE FUNCTION public.grant_initial_admin();
@@ -0,0 +1,5 @@
1
+
2
+ REVOKE ALL ON FUNCTION public.touch_content_sections() FROM PUBLIC, anon, authenticated;
3
+ REVOKE ALL ON FUNCTION public.grant_initial_admin() FROM PUBLIC, anon, authenticated;
4
+ REVOKE ALL ON FUNCTION public.has_role(uuid, public.app_role) FROM PUBLIC;
5
+ GRANT EXECUTE ON FUNCTION public.has_role(uuid, public.app_role) TO authenticated;
@@ -0,0 +1 @@
1
+ REVOKE EXECUTE ON FUNCTION public.has_role(uuid, public.app_role) FROM anon;
@@ -0,0 +1,92 @@
1
+ -- Additive: per-page localized SEO overrides + slug-change redirects.
2
+ -- Never touches customer submission data.
3
+
4
+ CREATE TABLE public.page_seo (
5
+ page_key text PRIMARY KEY,
6
+ slugs jsonb NOT NULL DEFAULT '{}'::jsonb, -- { sv: "boka", en: "book" }
7
+ seo jsonb NOT NULL DEFAULT '{}'::jsonb, -- { sv: { title, description, ogImage, ... }, en: {...} }
8
+ updated_at timestamptz NOT NULL DEFAULT now(),
9
+ updated_by uuid
10
+ );
11
+
12
+ GRANT SELECT ON public.page_seo TO anon, authenticated;
13
+ GRANT INSERT, UPDATE, DELETE ON public.page_seo TO authenticated;
14
+ GRANT ALL ON public.page_seo TO service_role;
15
+
16
+ ALTER TABLE public.page_seo ENABLE ROW LEVEL SECURITY;
17
+
18
+ CREATE POLICY "Anyone can read page_seo"
19
+ ON public.page_seo FOR SELECT
20
+ TO anon, authenticated
21
+ USING (true);
22
+
23
+ CREATE POLICY "Admins insert page_seo"
24
+ ON public.page_seo FOR INSERT
25
+ TO authenticated
26
+ WITH CHECK (has_role(auth.uid(), 'admin'::app_role));
27
+
28
+ CREATE POLICY "Admins update page_seo"
29
+ ON public.page_seo FOR UPDATE
30
+ TO authenticated
31
+ USING (has_role(auth.uid(), 'admin'::app_role))
32
+ WITH CHECK (has_role(auth.uid(), 'admin'::app_role));
33
+
34
+ CREATE POLICY "Admins delete page_seo"
35
+ ON public.page_seo FOR DELETE
36
+ TO authenticated
37
+ USING (has_role(auth.uid(), 'admin'::app_role));
38
+
39
+ CREATE OR REPLACE FUNCTION public.touch_page_seo()
40
+ RETURNS trigger
41
+ LANGUAGE plpgsql
42
+ SET search_path = public
43
+ AS $$
44
+ BEGIN
45
+ NEW.updated_at = now();
46
+ RETURN NEW;
47
+ END;
48
+ $$;
49
+
50
+ REVOKE EXECUTE ON FUNCTION public.touch_page_seo() FROM public, anon;
51
+
52
+ CREATE TRIGGER page_seo_touch
53
+ BEFORE UPDATE ON public.page_seo
54
+ FOR EACH ROW EXECUTE FUNCTION public.touch_page_seo();
55
+
56
+ -- Slug-change redirects. Append-only from the app's perspective;
57
+ -- edits/deletes require admin. Old URLs stay resolvable forever.
58
+ CREATE TABLE public.redirects (
59
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
60
+ from_path text NOT NULL UNIQUE, -- e.g. "/boka"
61
+ to_path text NOT NULL, -- e.g. "/bokning" or "/en/book"
62
+ status smallint NOT NULL DEFAULT 301,
63
+ created_at timestamptz NOT NULL DEFAULT now(),
64
+ created_by uuid
65
+ );
66
+
67
+ GRANT SELECT ON public.redirects TO anon, authenticated;
68
+ GRANT INSERT, UPDATE, DELETE ON public.redirects TO authenticated;
69
+ GRANT ALL ON public.redirects TO service_role;
70
+
71
+ ALTER TABLE public.redirects ENABLE ROW LEVEL SECURITY;
72
+
73
+ CREATE POLICY "Anyone can read redirects"
74
+ ON public.redirects FOR SELECT
75
+ TO anon, authenticated
76
+ USING (true);
77
+
78
+ CREATE POLICY "Admins insert redirects"
79
+ ON public.redirects FOR INSERT
80
+ TO authenticated
81
+ WITH CHECK (has_role(auth.uid(), 'admin'::app_role));
82
+
83
+ CREATE POLICY "Admins update redirects"
84
+ ON public.redirects FOR UPDATE
85
+ TO authenticated
86
+ USING (has_role(auth.uid(), 'admin'::app_role))
87
+ WITH CHECK (has_role(auth.uid(), 'admin'::app_role));
88
+
89
+ CREATE POLICY "Admins delete redirects"
90
+ ON public.redirects FOR DELETE
91
+ TO authenticated
92
+ USING (has_role(auth.uid(), 'admin'::app_role));
@@ -0,0 +1 @@
1
+ REVOKE EXECUTE ON FUNCTION public.touch_page_seo() FROM authenticated;
@@ -0,0 +1,11 @@
1
+ CREATE OR REPLACE FUNCTION public.touch_page_seo()
2
+ RETURNS trigger
3
+ LANGUAGE plpgsql
4
+ SECURITY INVOKER
5
+ SET search_path = public
6
+ AS $$
7
+ BEGIN
8
+ NEW.updated_at = now();
9
+ RETURN NEW;
10
+ END;
11
+ $$;
@@ -0,0 +1,63 @@
1
+ CREATE TABLE public.locale_domains (
2
+ locale text NOT NULL PRIMARY KEY,
3
+ domain text NOT NULL,
4
+ is_default boolean NOT NULL DEFAULT false,
5
+ is_admin_domain boolean NOT NULL DEFAULT false,
6
+ created_at timestamp with time zone NOT NULL DEFAULT now(),
7
+ updated_at timestamp with time zone NOT NULL DEFAULT now()
8
+ );
9
+
10
+ GRANT SELECT ON public.locale_domains TO anon;
11
+ GRANT SELECT ON public.locale_domains TO authenticated;
12
+ GRANT ALL ON public.locale_domains TO service_role;
13
+
14
+ ALTER TABLE public.locale_domains ENABLE ROW LEVEL SECURITY;
15
+
16
+ CREATE POLICY "Anyone can read locale domains"
17
+ ON public.locale_domains
18
+ FOR SELECT
19
+ TO anon, authenticated
20
+ USING (true);
21
+
22
+ CREATE POLICY "Only admins can insert locale domains"
23
+ ON public.locale_domains
24
+ FOR INSERT
25
+ TO authenticated
26
+ WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
27
+
28
+ CREATE POLICY "Only admins can update locale domains"
29
+ ON public.locale_domains
30
+ FOR UPDATE
31
+ TO authenticated
32
+ USING (public.has_role(auth.uid(), 'admin'::public.app_role))
33
+ WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role));
34
+
35
+ CREATE POLICY "Only admins can delete locale domains"
36
+ ON public.locale_domains
37
+ FOR DELETE
38
+ TO authenticated
39
+ USING (public.has_role(auth.uid(), 'admin'::public.app_role));
40
+
41
+ CREATE OR REPLACE FUNCTION public.touch_locale_domains()
42
+ RETURNS trigger
43
+ LANGUAGE plpgsql
44
+ SET search_path TO 'public'
45
+ AS $$
46
+ BEGIN
47
+ NEW.updated_at = now();
48
+ RETURN NEW;
49
+ END;
50
+ $$;
51
+
52
+ CREATE TRIGGER update_locale_domains_updated_at
53
+ BEFORE UPDATE ON public.locale_domains
54
+ FOR EACH ROW
55
+ EXECUTE FUNCTION public.touch_locale_domains();
56
+
57
+ INSERT INTO public.locale_domains (locale, domain, is_default, is_admin_domain)
58
+ VALUES ('sv', 'www.dev.barskola.nu', true, true)
59
+ ON CONFLICT (locale) DO UPDATE SET
60
+ domain = EXCLUDED.domain,
61
+ is_default = EXCLUDED.is_default,
62
+ is_admin_domain = EXCLUDED.is_admin_domain,
63
+ updated_at = now();
@@ -0,0 +1,209 @@
1
+
2
+ -- =========================================================
3
+ -- PAGES
4
+ -- =========================================================
5
+ CREATE TABLE public.pages (
6
+ id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
7
+ key text NOT NULL UNIQUE,
8
+ name text NOT NULL,
9
+ is_system boolean NOT NULL DEFAULT false,
10
+ status text NOT NULL DEFAULT 'draft',
11
+ current_version_id uuid,
12
+ created_at timestamptz NOT NULL DEFAULT now(),
13
+ updated_at timestamptz NOT NULL DEFAULT now(),
14
+ updated_by uuid
15
+ );
16
+ GRANT SELECT ON public.pages TO anon;
17
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.pages TO authenticated;
18
+ GRANT ALL ON public.pages TO service_role;
19
+ ALTER TABLE public.pages ENABLE ROW LEVEL SECURITY;
20
+
21
+ CREATE POLICY "Anyone can read pages" ON public.pages FOR SELECT USING (true);
22
+ CREATE POLICY "Admins insert pages" ON public.pages FOR INSERT TO authenticated
23
+ WITH CHECK (has_role(auth.uid(),'admin'::app_role));
24
+ CREATE POLICY "Admins update pages" ON public.pages FOR UPDATE TO authenticated
25
+ USING (has_role(auth.uid(),'admin'::app_role))
26
+ WITH CHECK (has_role(auth.uid(),'admin'::app_role));
27
+ CREATE POLICY "Admins delete pages" ON public.pages FOR DELETE TO authenticated
28
+ USING (has_role(auth.uid(),'admin'::app_role) AND is_system = false);
29
+
30
+ -- =========================================================
31
+ -- PAGE VERSIONS
32
+ -- =========================================================
33
+ CREATE TABLE public.page_versions (
34
+ id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
35
+ page_id uuid NOT NULL REFERENCES public.pages(id) ON DELETE CASCADE,
36
+ version_no integer NOT NULL,
37
+ blocks jsonb NOT NULL DEFAULT '[]'::jsonb,
38
+ note text,
39
+ created_at timestamptz NOT NULL DEFAULT now(),
40
+ created_by uuid,
41
+ UNIQUE (page_id, version_no)
42
+ );
43
+ GRANT SELECT ON public.page_versions TO anon;
44
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.page_versions TO authenticated;
45
+ GRANT ALL ON public.page_versions TO service_role;
46
+ ALTER TABLE public.page_versions ENABLE ROW LEVEL SECURITY;
47
+
48
+ CREATE POLICY "Anyone can read page_versions" ON public.page_versions FOR SELECT USING (true);
49
+ CREATE POLICY "Admins insert page_versions" ON public.page_versions FOR INSERT TO authenticated
50
+ WITH CHECK (has_role(auth.uid(),'admin'::app_role));
51
+ CREATE POLICY "Admins update page_versions" ON public.page_versions FOR UPDATE TO authenticated
52
+ USING (has_role(auth.uid(),'admin'::app_role))
53
+ WITH CHECK (has_role(auth.uid(),'admin'::app_role));
54
+ CREATE POLICY "Admins delete page_versions" ON public.page_versions FOR DELETE TO authenticated
55
+ USING (has_role(auth.uid(),'admin'::app_role));
56
+
57
+ ALTER TABLE public.pages
58
+ ADD CONSTRAINT pages_current_version_fk
59
+ FOREIGN KEY (current_version_id) REFERENCES public.page_versions(id) ON DELETE SET NULL;
60
+
61
+ -- =========================================================
62
+ -- PAGE PUBLICATIONS
63
+ -- =========================================================
64
+ CREATE TABLE public.page_publications (
65
+ id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
66
+ page_id uuid NOT NULL REFERENCES public.pages(id) ON DELETE CASCADE,
67
+ version_id uuid NOT NULL REFERENCES public.page_versions(id) ON DELETE CASCADE,
68
+ publish_at timestamptz NOT NULL DEFAULT now(),
69
+ unpublish_at timestamptz,
70
+ is_active boolean NOT NULL DEFAULT true,
71
+ published_at timestamptz NOT NULL DEFAULT now(),
72
+ published_by uuid,
73
+ note text
74
+ );
75
+ CREATE INDEX page_publications_active_idx ON public.page_publications(page_id) WHERE is_active;
76
+ GRANT SELECT ON public.page_publications TO anon;
77
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.page_publications TO authenticated;
78
+ GRANT ALL ON public.page_publications TO service_role;
79
+ ALTER TABLE public.page_publications ENABLE ROW LEVEL SECURITY;
80
+
81
+ CREATE POLICY "Anyone can read page_publications" ON public.page_publications FOR SELECT USING (true);
82
+ CREATE POLICY "Admins manage page_publications" ON public.page_publications FOR ALL TO authenticated
83
+ USING (has_role(auth.uid(),'admin'::app_role))
84
+ WITH CHECK (has_role(auth.uid(),'admin'::app_role));
85
+
86
+ -- =========================================================
87
+ -- GLOBAL SECTIONS
88
+ -- =========================================================
89
+ CREATE TABLE public.global_sections (
90
+ id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
91
+ key text NOT NULL UNIQUE,
92
+ name text NOT NULL,
93
+ current_version_id uuid,
94
+ created_at timestamptz NOT NULL DEFAULT now(),
95
+ updated_at timestamptz NOT NULL DEFAULT now(),
96
+ updated_by uuid
97
+ );
98
+ GRANT SELECT ON public.global_sections TO anon;
99
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.global_sections TO authenticated;
100
+ GRANT ALL ON public.global_sections TO service_role;
101
+ ALTER TABLE public.global_sections ENABLE ROW LEVEL SECURITY;
102
+
103
+ CREATE POLICY "Anyone can read global_sections" ON public.global_sections FOR SELECT USING (true);
104
+ CREATE POLICY "Admins manage global_sections" ON public.global_sections FOR ALL TO authenticated
105
+ USING (has_role(auth.uid(),'admin'::app_role))
106
+ WITH CHECK (has_role(auth.uid(),'admin'::app_role));
107
+
108
+ CREATE TABLE public.global_section_versions (
109
+ id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
110
+ section_id uuid NOT NULL REFERENCES public.global_sections(id) ON DELETE CASCADE,
111
+ version_no integer NOT NULL,
112
+ blocks jsonb NOT NULL DEFAULT '[]'::jsonb,
113
+ note text,
114
+ created_at timestamptz NOT NULL DEFAULT now(),
115
+ created_by uuid,
116
+ UNIQUE (section_id, version_no)
117
+ );
118
+ GRANT SELECT ON public.global_section_versions TO anon;
119
+ GRANT SELECT, INSERT, UPDATE, DELETE ON public.global_section_versions TO authenticated;
120
+ GRANT ALL ON public.global_section_versions TO service_role;
121
+ ALTER TABLE public.global_section_versions ENABLE ROW LEVEL SECURITY;
122
+
123
+ CREATE POLICY "Anyone can read global_section_versions" ON public.global_section_versions FOR SELECT USING (true);
124
+ CREATE POLICY "Admins manage global_section_versions" ON public.global_section_versions FOR ALL TO authenticated
125
+ USING (has_role(auth.uid(),'admin'::app_role))
126
+ WITH CHECK (has_role(auth.uid(),'admin'::app_role));
127
+
128
+ ALTER TABLE public.global_sections
129
+ ADD CONSTRAINT global_sections_current_version_fk
130
+ FOREIGN KEY (current_version_id) REFERENCES public.global_section_versions(id) ON DELETE SET NULL;
131
+
132
+ -- =========================================================
133
+ -- updated_at triggers
134
+ -- =========================================================
135
+ CREATE OR REPLACE FUNCTION public.touch_pages() RETURNS trigger
136
+ LANGUAGE plpgsql SET search_path = public AS $$
137
+ BEGIN NEW.updated_at = now(); NEW.updated_by = auth.uid(); RETURN NEW; END; $$;
138
+ CREATE TRIGGER touch_pages_upd BEFORE UPDATE ON public.pages
139
+ FOR EACH ROW EXECUTE FUNCTION public.touch_pages();
140
+
141
+ CREATE OR REPLACE FUNCTION public.touch_global_sections() RETURNS trigger
142
+ LANGUAGE plpgsql SET search_path = public AS $$
143
+ BEGIN NEW.updated_at = now(); NEW.updated_by = auth.uid(); RETURN NEW; END; $$;
144
+ CREATE TRIGGER touch_global_sections_upd BEFORE UPDATE ON public.global_sections
145
+ FOR EACH ROW EXECUTE FUNCTION public.touch_global_sections();
146
+
147
+ -- =========================================================
148
+ -- Seed built-in system pages with an initial empty version each
149
+ -- =========================================================
150
+ DO $$
151
+ DECLARE
152
+ page_rec RECORD;
153
+ new_page_id uuid;
154
+ new_version_id uuid;
155
+ seed_pages jsonb := '[
156
+ {"key":"home","name":"Home","blocks":[
157
+ {"id":"hero","type":"hero","props":{}},
158
+ {"id":"stats","type":"stats","props":{}},
159
+ {"id":"intro","type":"intro","props":{}},
160
+ {"id":"features","type":"features","props":{}},
161
+ {"id":"destinations","type":"destinations","props":{}},
162
+ {"id":"packages","type":"packages","props":{}},
163
+ {"id":"gallery","type":"gallery","props":{}},
164
+ {"id":"testimonials","type":"testimonials","props":{}},
165
+ {"id":"cta","type":"contactCta","props":{}}
166
+ ]},
167
+ {"key":"utbildningen","name":"Utbildningen","blocks":[
168
+ {"id":"intro","type":"intro","props":{}},
169
+ {"id":"features","type":"features","props":{}}
170
+ ]},
171
+ {"key":"destinationer","name":"Destinationer","blocks":[
172
+ {"id":"destinations","type":"destinations","props":{}}
173
+ ]},
174
+ {"key":"datum","name":"Datum","blocks":[
175
+ {"id":"packages","type":"packages","props":{}}
176
+ ]},
177
+ {"key":"om","name":"Om","blocks":[
178
+ {"id":"intro","type":"intro","props":{}},
179
+ {"id":"stats","type":"stats","props":{}},
180
+ {"id":"gallery","type":"gallery","props":{}}
181
+ ]},
182
+ {"key":"kontakt","name":"Kontakt","blocks":[
183
+ {"id":"cta","type":"contactCta","props":{}}
184
+ ]}
185
+ ]'::jsonb;
186
+ item jsonb;
187
+ BEGIN
188
+ FOR item IN SELECT * FROM jsonb_array_elements(seed_pages) LOOP
189
+ INSERT INTO public.pages(key,name,is_system,status)
190
+ VALUES (item->>'key', item->>'name', true, 'published')
191
+ ON CONFLICT (key) DO NOTHING
192
+ RETURNING id INTO new_page_id;
193
+
194
+ IF new_page_id IS NULL THEN
195
+ SELECT id INTO new_page_id FROM public.pages WHERE key = item->>'key';
196
+ END IF;
197
+
198
+ IF NOT EXISTS (SELECT 1 FROM public.page_versions WHERE page_id = new_page_id) THEN
199
+ INSERT INTO public.page_versions(page_id, version_no, blocks, note)
200
+ VALUES (new_page_id, 1, item->'blocks', 'Initial seed')
201
+ RETURNING id INTO new_version_id;
202
+
203
+ UPDATE public.pages SET current_version_id = new_version_id WHERE id = new_page_id;
204
+
205
+ INSERT INTO public.page_publications(page_id, version_id, is_active, note)
206
+ VALUES (new_page_id, new_version_id, true, 'Initial publish');
207
+ END IF;
208
+ END LOOP;
209
+ END $$;
@@ -0,0 +1,17 @@
1
+
2
+ CREATE POLICY "Admins can read media"
3
+ ON storage.objects FOR SELECT TO authenticated
4
+ USING (bucket_id = 'media' AND public.has_role(auth.uid(), 'admin'));
5
+
6
+ CREATE POLICY "Admins can upload media"
7
+ ON storage.objects FOR INSERT TO authenticated
8
+ WITH CHECK (bucket_id = 'media' AND public.has_role(auth.uid(), 'admin'));
9
+
10
+ CREATE POLICY "Admins can update media"
11
+ ON storage.objects FOR UPDATE TO authenticated
12
+ USING (bucket_id = 'media' AND public.has_role(auth.uid(), 'admin'))
13
+ WITH CHECK (bucket_id = 'media' AND public.has_role(auth.uid(), 'admin'));
14
+
15
+ CREATE POLICY "Admins can delete media"
16
+ ON storage.objects FOR DELETE TO authenticated
17
+ USING (bucket_id = 'media' AND public.has_role(auth.uid(), 'admin'));
@@ -0,0 +1,37 @@
1
+
2
+ CREATE TABLE public.block_templates (
3
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ name text NOT NULL,
5
+ description text,
6
+ block_type text NOT NULL,
7
+ block jsonb NOT NULL,
8
+ created_at timestamptz NOT NULL DEFAULT now(),
9
+ updated_at timestamptz NOT NULL DEFAULT now(),
10
+ created_by uuid
11
+ );
12
+
13
+ GRANT SELECT ON public.block_templates TO anon, authenticated;
14
+ GRANT INSERT, UPDATE, DELETE ON public.block_templates TO authenticated;
15
+ GRANT ALL ON public.block_templates TO service_role;
16
+
17
+ ALTER TABLE public.block_templates ENABLE ROW LEVEL SECURITY;
18
+
19
+ CREATE POLICY "Anyone can read block_templates"
20
+ ON public.block_templates FOR SELECT USING (true);
21
+
22
+ CREATE POLICY "Admins insert block_templates"
23
+ ON public.block_templates FOR INSERT TO authenticated
24
+ WITH CHECK (public.has_role(auth.uid(), 'admin'));
25
+
26
+ CREATE POLICY "Admins update block_templates"
27
+ ON public.block_templates FOR UPDATE TO authenticated
28
+ USING (public.has_role(auth.uid(), 'admin'))
29
+ WITH CHECK (public.has_role(auth.uid(), 'admin'));
30
+
31
+ CREATE POLICY "Admins delete block_templates"
32
+ ON public.block_templates FOR DELETE TO authenticated
33
+ USING (public.has_role(auth.uid(), 'admin'));
34
+
35
+ CREATE TRIGGER touch_block_templates_updated_at
36
+ BEFORE UPDATE ON public.block_templates
37
+ FOR EACH ROW EXECUTE FUNCTION public.touch_pages();
@@ -0,0 +1,5 @@
1
+
2
+ -- 1. Extend role enum
3
+ ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'superadmin';
4
+ ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'seo';
5
+ ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'user';