@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.
- package/migrations/20260728113741_f9744b19-078b-46e5-b00f-31c84aaf71ab.sql +111 -0
- package/migrations/20260728113757_4156a886-21fb-4a1f-9c12-acb7bb145102.sql +5 -0
- package/migrations/20260728113823_6e0237e1-99e3-4e1a-8a68-804b7bcdbbb8.sql +1 -0
- package/migrations/20260728115305_506d92b4-129f-4d2b-961f-cf76e592677e.sql +92 -0
- package/migrations/20260728115320_717d4447-f433-4a1d-89d2-fc8f028d4ac6.sql +1 -0
- package/migrations/20260728115347_c77f5a56-19db-4117-bb60-9ad692b817ab.sql +11 -0
- package/migrations/20260728132052_b692dcfe-a944-4a19-afd0-187f7600feb8.sql +63 -0
- package/migrations/20260728183318_05b05a47-ba05-4a66-ab8f-69f2688fea84.sql +209 -0
- package/migrations/20260728193035_84b68d27-c3f3-4516-a3a9-15fc6f476163.sql +17 -0
- package/migrations/20260728193707_8438ee2d-d589-4048-86e2-464fd209af80.sql +37 -0
- package/migrations/20260729055312_08862d7e-0279-4d4f-8709-a59b9f068c55.sql +5 -0
- package/migrations/20260729055348_418b7e36-4877-4304-831f-290ca73e56f3.sql +133 -0
- package/migrations/20260729055358_20b467e2-4efd-42c1-bd34-773ed6b77fc5.sql +3 -0
- package/migrations/20260729061039_306859eb-a38c-44ef-af1a-6c02a8fd84db.sql +43 -0
- package/migrations/20260730065600_54c25861-3355-4600-a72f-3d67b457ecb1.sql +54 -0
- package/migrations/20260730071745_d4904314-15ff-4c58-ac66-85e7371e04b5.sql +113 -0
- package/migrations/20260730071914_48163aa3-830e-4dcc-bdd4-c35be282ab87.sql +22 -0
- package/migrations/20260730085949_bcfc3270-ea80-4617-86a3-2555ed34a280.sql +24 -0
- package/migrations/20260730095256_c977c30e-b285-4041-8804-c0bd72b6d9b3.sql +13 -0
- package/migrations/20260730100057_e341b887-eb0e-4a74-850e-0ee5ffb70d7a.sql +38 -0
- package/migrations/20260730103703_d5c4e1b7-1e2c-4dd0-be4e-185c159ade69.sql +62 -0
- package/migrations/20260730105635_3557b7da-919e-48db-8c92-8221d68d3cf9.sql +29 -0
- package/migrations/20260730120308_12ff8cfb-43f9-4070-abde-61531ad3b75f.sql +73 -0
- package/migrations/20260730125024_1baf8926-12a2-48f4-8edc-7309941309e9.sql +18 -0
- package/migrations/20260730173510_feb974d1-510e-4f52-9e20-5fd0582d8952.sql +107 -0
- package/migrations/20260730175409_9f60743f-f5e1-418c-a700-c91fb3704db8.sql +9 -0
- package/migrations/20260730181224_1c52ff16-c7a2-4ef1-a0a8-e493c2c9106f.sql +7 -0
- package/migrations/20260730183129_933adb75-f985-4d82-ad5d-213b69fd42e5.sql +32 -0
- package/migrations/20260730183918_7bc7a827-2100-49f1-bc6e-24509249350b.sql +68 -0
- package/migrations/20260730202628_7ceade5e-644c-4bc2-91b8-594133329f31.sql +1 -0
- package/migrations/20260730211056_57f42049-00b4-473b-a2cd-65a13570b8d2.sql +16 -0
- package/migrations/20260730214340_98af2e55-9031-4c11-9659-c47a37a8d6b7.sql +1 -0
- package/migrations/20260731091630_8a6f28ce-5833-4901-ab17-27648cef32ec.sql +149 -0
- package/migrations/20260731094716_4c68a1df-83ed-4ba8-8019-fa2fe2b9681e.sql +65 -0
- package/migrations/20260731191655_fef97c3c-1397-47b4-8b56-afb020298e97.sql +165 -0
- package/migrations/20260731192212_95772a2f-19a1-4013-b463-2eb161e96737.sql +135 -0
- package/migrations/20260731192815_c490653e-1232-4ad9-a150-3c83d2b90f5e.sql +13 -0
- package/migrations/20260731193135_7966e740-5e86-4b14-8e1d-5119069666ba.sql +2 -0
- package/migrations/20260731193245_599790d0-ca5e-4b5d-a3e7-d69b2a79e1a3.sql +13 -0
- package/migrations/20260731212959_a748cb0d-4dc7-45dc-be90-f6bacbfab26c.sql +9 -0
- package/migrations/20260801003057_1195f4c1-fb54-42dc-9718-638a25e6a82e.sql +3 -0
- package/migrations/20260801003300_fc591960-cb41-41cf-837d-2eef23b29ca9.sql +40 -0
- package/migrations/20260802151445_a56b0023-eaf7-41e2-a7fb-d4beddfc587c.sql +3 -0
- package/migrations/20260802202621_d661af8a-8513-4b54-b375-f963ae824c8f.sql +225 -0
- package/migrations/20260802202702_c30cd837-744c-4641-85f1-edd3a7ebbcb6.sql +16 -0
- package/migrations/20260802202720_4418d18d-c851-42dc-9345-78fc330128ae.sql +2 -0
- package/migrations/20260803132514_78ba1f53-c85b-4457-96b1-7d49c5c8b494.sql +34 -0
- package/migrations/20260803181201_28c8aa09-baa8-431c-8338-90b1af9c8172.sql +22 -0
- package/migrations/20260803201518_6e8a6028-b70f-4a7d-8bc9-95c86304e7ae.sql +12 -0
- package/migrations/20260803201535_32a9a213-cb92-4035-b8cd-8211ca34af68.sql +12 -0
- package/migrations/20260811103505_29df753d-14fa-4a05-8fea-f4207785fba3.sql +35 -0
- package/migrations/20260811112455_217c4028-f587-4c98-9e4f-9e6c452ab761.sql +20 -0
- package/migrations/20260811114438_6687d67d-bad3-4803-9638-8b7670d2e5d4.sql +86 -0
- package/migrations/20260811130318_7cb95e95-9baa-40af-8f3f-de1d4e09cec4.sql +1 -0
- package/migrations/20260811220548_80d99ec2-bd4b-475d-a627-9408bb60ce56.sql +1 -0
- package/migrations/20260811222519_6b040f56-4de3-49e0-92e4-d9deec5ead6a.sql +3 -0
- package/migrations/20260812000000_seed_voyara_content.sql +390 -0
- package/migrations/20260818040722_1bdf1b8f-7fb0-4ae8-8ecf-73f1fadff3c2.sql +2 -0
- package/migrations/20260818040808_69a5e832-57e8-4aa7-aff8-7fd7777faa7c.sql +15 -0
- package/migrations/20260818040839_e6523163-bbf5-4a39-9233-91759059569a.sql +26 -0
- package/package.json +8 -4
- package/src/backoffice/media/ReviewImageUpload.tsx +104 -0
- package/src/platform/contract.ts +92 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
insert into public.pages (key, name, status)
|
|
2
|
+
values ('ayia-napa', 'Ayia Napa', 'published')
|
|
3
|
+
on conflict (key) do nothing;
|
|
4
|
+
|
|
5
|
+
with pg as (
|
|
6
|
+
select id from public.pages where key = 'ayia-napa'
|
|
7
|
+
), nv as (
|
|
8
|
+
insert into public.page_versions (page_id, version_no, note, blocks)
|
|
9
|
+
select pg.id,
|
|
10
|
+
coalesce((select max(version_no) from public.page_versions where page_id = pg.id), 0) + 1,
|
|
11
|
+
'Rebuilt from barskola.nu/ayia-napa',
|
|
12
|
+
jsonb_build_array(
|
|
13
|
+
jsonb_build_object(
|
|
14
|
+
'id','an-hero','type','heroFull','visible',true,'props',jsonb_build_object(
|
|
15
|
+
'mediaType','image',
|
|
16
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/ayia-napa-hero-1920.webp',
|
|
17
|
+
'imageMobile','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/ayia-napa-hero-1920.webp',
|
|
18
|
+
'imageAlt', jsonb_build_object('sv','Solnedgång över stranden i Ayia Napa på Cypern'),
|
|
19
|
+
'eyebrow', jsonb_build_object('sv','Cypern'),
|
|
20
|
+
'title', jsonb_build_object('sv','Ayia Napa'),
|
|
21
|
+
'body', jsonb_build_object('sv','Europas vackraste stränder, 27 grader redan i maj och ett nattliv som aldrig sover — mitt i din bartenderutbildning.'),
|
|
22
|
+
'primaryCta', jsonb_build_object('label', jsonb_build_object('sv','Se kursdatum'), 'href','/datum'),
|
|
23
|
+
'secondaryCta', jsonb_build_object('label', jsonb_build_object('sv','Om bartenderkursen'), 'href','/bartenderkurs'),
|
|
24
|
+
'height','tall','overlay','bottom','textAlign','left','verticalAlign','bottom',
|
|
25
|
+
'textTone','light','contentWidth','normal','titleSize','xl','titleFont','serif',
|
|
26
|
+
'showScrollCue',true,'anchor','ayia-napa'
|
|
27
|
+
)
|
|
28
|
+
),
|
|
29
|
+
jsonb_build_object(
|
|
30
|
+
'id','an-intro','type','prose','visible',true,'props',jsonb_build_object(
|
|
31
|
+
'eyebrow', jsonb_build_object('sv','Destination'),
|
|
32
|
+
'title', jsonb_build_object('sv','Allt du kan uppleva i Ayia Napa'),
|
|
33
|
+
'lead', jsonb_build_object('sv','I Ayia Napa finns många olika aktiviteter att göra. Beachvolleyboll, beerpong, hopp från klippor med mera. Självklart erbjuder Ayia Napa mer än bara fest och party.'),
|
|
34
|
+
'tone','white','layout','two-col',
|
|
35
|
+
'items', jsonb_build_array(
|
|
36
|
+
jsonb_build_object('id','an-i1','heading',jsonb_build_object('sv','Mer än bara nattliv'),'body',jsonb_build_object('sv','Utöver alla barer och nattklubbar kan man hitta på mängder med aktiviteter till bra priser. I utkanten av Ayia Napa ligger vattenparken Waterworld, en av Europas absolut största vattenparker. Temat för parken är grekisk mytologi och här finner du bland annat Apollos rutschkana, Aeolos virvelpool och Poseidons vågpool.')),
|
|
37
|
+
jsonb_build_object('id','an-i2','heading',jsonb_build_object('sv','Aktiviteter under kursen'),'body',jsonb_build_object('sv','Man kan även spela padel på Ayia Napa Padel Club som har fyra padelbanor. Därtill finns det Bungy Jump, nöjesfält och diverse uthyrning av mopeder, fyrhjulingar och beach buggys. Nedan listar vi några av de populäraste aktiviteterna som du kan uppleva samtidigt som du går bartenderkursen. Observera att eventen sker mot en kostnad och att vissa event är säsongsbetonade och inte erbjuds under alla kurser.'))
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
),
|
|
41
|
+
jsonb_build_object(
|
|
42
|
+
'id','an-strander','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
43
|
+
'style','editorial','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','paper','aspect','landscape','rounded',false,
|
|
44
|
+
'mediaType','image',
|
|
45
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/nissi-beach-1-1920.webp',
|
|
46
|
+
'imageAlt', jsonb_build_object('sv','Nissi Beach i Ayia Napa med turkost vatten och vit sand'),
|
|
47
|
+
'eyebrow', jsonb_build_object('sv','Stränder'),
|
|
48
|
+
'title', jsonb_build_object('sv','Europas bästa stränder'),
|
|
49
|
+
'body', jsonb_build_object('sv','Ayia Napa är ett paradis för den som söker vackra stränder, sol och upplevelser. Med Nissi Beach som har framröstats flera år i rad som en av Europas vackraste stränder och med en medeltemperatur på 27 grader redan i maj månad är det det mest självklara valet av semesterort.')
|
|
50
|
+
)
|
|
51
|
+
),
|
|
52
|
+
jsonb_build_object(
|
|
53
|
+
'id','an-nattliv','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
54
|
+
'style','dark','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','ink','aspect','landscape','rounded',false,
|
|
55
|
+
'mediaType','image',
|
|
56
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/ayia-napa-nattliv-1920.webp',
|
|
57
|
+
'imageAlt', jsonb_build_object('sv','Nattliv i Ayia Napa med fullsatt bargata'),
|
|
58
|
+
'eyebrow', jsonb_build_object('sv','Nattliv'),
|
|
59
|
+
'title', jsonb_build_object('sv','Fantastiskt nattliv'),
|
|
60
|
+
'body', jsonb_build_object('sv','För den som söker festligheter och upplevelser går ingen besviken. Med över 200 barer och nattklubbar i Ayia Napa finns det alltid något att välja på, även för den mest kräsna festprissen. Väl på plats finns det även en rad event så som barrundor, partybåtar och poolpartyn. Dessa sker veckovis och är extremt uppskattat bland både turister och de som jobbar på plats.')
|
|
61
|
+
)
|
|
62
|
+
),
|
|
63
|
+
jsonb_build_object(
|
|
64
|
+
'id','an-partybat','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
65
|
+
'style','editorial','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','white','aspect','landscape','rounded',false,
|
|
66
|
+
'mediaType','image',
|
|
67
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/partybat-ayia-napa-1920.webp',
|
|
68
|
+
'imageAlt', jsonb_build_object('sv','Partybåt utanför Ayia Napa med dansande gäster'),
|
|
69
|
+
'eyebrow', jsonb_build_object('sv','Event'),
|
|
70
|
+
'title', jsonb_build_object('sv','Partybåt'),
|
|
71
|
+
'body', jsonb_build_object('sv','Partybåten är en fantastisk upplevelse ute till havs med sol, bad, tävlingar och massa party. DJ:n spelar sommarens absolut hetaste låtar och dansgolvet kokar. Partybåten som är ute till havs i cirka 3,5 timmar brukar ses som den mest uppskattade aktiviteten i Ayia Napa.')
|
|
72
|
+
)
|
|
73
|
+
),
|
|
74
|
+
jsonb_build_object(
|
|
75
|
+
'id','an-poolparty','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
76
|
+
'style','editorial','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','paper','aspect','landscape','rounded',false,
|
|
77
|
+
'mediaType','image',
|
|
78
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/poolparty-ayia-napa-1920.webp',
|
|
79
|
+
'imageAlt', jsonb_build_object('sv','Poolparty på Pambos Napa Rocks i Ayia Napa'),
|
|
80
|
+
'eyebrow', jsonb_build_object('sv','Event'),
|
|
81
|
+
'title', jsonb_build_object('sv','Poolparty'),
|
|
82
|
+
'body', jsonb_build_object('sv','Poolpartyt håller hus på hotellet Pambos Napa Rocks som är ett av Ayia Napas största hotell. Med ett poolområde som rymmer 2 000 personer är det en given plats att ha eventet på.')
|
|
83
|
+
)
|
|
84
|
+
),
|
|
85
|
+
jsonb_build_object(
|
|
86
|
+
'id','an-waterworld','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
87
|
+
'style','editorial','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','white','aspect','landscape','rounded',false,
|
|
88
|
+
'mediaType','image',
|
|
89
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/waterworld-ayia-napa-1920.webp',
|
|
90
|
+
'imageAlt', jsonb_build_object('sv','Vattenrutschkanor i vattenparken Waterworld på Cypern'),
|
|
91
|
+
'eyebrow', jsonb_build_object('sv','Aktivitet'),
|
|
92
|
+
'title', jsonb_build_object('sv','Waterworld'),
|
|
93
|
+
'body', jsonb_build_object('sv','En dag med sol, bad och vattenattraktioner på Cyperns största vattenland. Parken består av mängder av pooler och olika vattenattraktioner för alla åldrar.')
|
|
94
|
+
)
|
|
95
|
+
),
|
|
96
|
+
jsonb_build_object(
|
|
97
|
+
'id','an-cape-greco','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
98
|
+
'style','editorial','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','paper','aspect','landscape','rounded',false,
|
|
99
|
+
'mediaType','image',
|
|
100
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/cape-greco-hoppklippor-1920.webp',
|
|
101
|
+
'imageAlt', jsonb_build_object('sv','Hoppklippor vid Cape Greco utanför Ayia Napa'),
|
|
102
|
+
'eyebrow', jsonb_build_object('sv','Aktivitet'),
|
|
103
|
+
'title', jsonb_build_object('sv','Cape Greco (hoppklippor)'),
|
|
104
|
+
'body', jsonb_build_object('sv','Cape Greco är ett naturreservat strax utanför Ayia Napa. Området har många sevärdheter att erbjuda som till exempel de drygt tolv meter höga klipporna. Dessa klippor ger möjligheten till att hoppa, se den vackra utsikten över Medelhavet eller klättra ner till de gigantiska grottorna.')
|
|
105
|
+
)
|
|
106
|
+
),
|
|
107
|
+
jsonb_build_object(
|
|
108
|
+
'id','an-barrunda','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
109
|
+
'style','dark','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','ink','aspect','landscape','rounded',false,
|
|
110
|
+
'mediaType','image',
|
|
111
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/barrunda-ayia-napa-1920.webp',
|
|
112
|
+
'imageAlt', jsonb_build_object('sv','Barrunda i Ayia Napa med glada deltagare'),
|
|
113
|
+
'eyebrow', jsonb_build_object('sv','Event'),
|
|
114
|
+
'title', jsonb_build_object('sv','Barrunda'),
|
|
115
|
+
'body', jsonb_build_object('sv','Barrundan är ett väldigt populärt event som erbjuds en gång per vecka och är något som Nordic Invasion arrangerar. På barrundan bjuds specialpriser, spex och sköna aktiviteter på varje bar.')
|
|
116
|
+
)
|
|
117
|
+
),
|
|
118
|
+
jsonb_build_object(
|
|
119
|
+
'id','an-snorkling','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
120
|
+
'style','editorial','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','white','aspect','landscape','rounded',false,
|
|
121
|
+
'mediaType','image',
|
|
122
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/snorkling-paddleboard-ayia-napa-1920.webp',
|
|
123
|
+
'imageAlt', jsonb_build_object('sv','Snorkling och paddleboard i det kristallklara vattnet i Ayia Napa'),
|
|
124
|
+
'eyebrow', jsonb_build_object('sv','Aktivitet'),
|
|
125
|
+
'title', jsonb_build_object('sv','Snorkling & paddleboard'),
|
|
126
|
+
'body', jsonb_build_object('sv','Stränderna i Ayia Napa är otroligt fina. På förfrågan ses vi en eftermiddag på någon av de fina stränderna i Ayia Napa där vi njuter av solen, snorklar i det kristallklara havet och paddlar paddleboard.')
|
|
127
|
+
)
|
|
128
|
+
),
|
|
129
|
+
jsonb_build_object(
|
|
130
|
+
'id','an-cta','type','ctaBanner','visible',true,'props',jsonb_build_object(
|
|
131
|
+
'tone','sea',
|
|
132
|
+
'eyebrow', jsonb_build_object('sv','Nästa säsong'),
|
|
133
|
+
'title', jsonb_build_object('sv','Upplev Ayia Napa som bartenderelev'),
|
|
134
|
+
'body', jsonb_build_object('sv','Fyra veckor med utbildning, praktik och allt det här — boende, jobbgaranti och internationellt diplom ingår.'),
|
|
135
|
+
'cta', jsonb_build_object('label', jsonb_build_object('sv','Se kursdatum'), 'href','/datum'),
|
|
136
|
+
'secondaryCta', jsonb_build_object('label', jsonb_build_object('sv','Kontakta oss'), 'href','/kontakt')
|
|
137
|
+
)
|
|
138
|
+
),
|
|
139
|
+
jsonb_build_object('id','an-contact','type','contactCta','visible',true,'props',jsonb_build_object())
|
|
140
|
+
)
|
|
141
|
+
from pg
|
|
142
|
+
returning id, page_id
|
|
143
|
+
)
|
|
144
|
+
insert into public.page_publications (page_id, version_id, note)
|
|
145
|
+
select nv.page_id, nv.id, 'Initial publish of /ayia-napa' from nv;
|
|
146
|
+
|
|
147
|
+
update public.pages p
|
|
148
|
+
set current_version_id = (
|
|
149
|
+
select v.id from public.page_versions v
|
|
150
|
+
where v.page_id = p.id order by v.version_no desc limit 1
|
|
151
|
+
), status = 'published'
|
|
152
|
+
where p.key = 'ayia-napa';
|
|
153
|
+
|
|
154
|
+
insert into public.page_seo (page_key, slugs, seo)
|
|
155
|
+
values (
|
|
156
|
+
'ayia-napa',
|
|
157
|
+
jsonb_build_object('sv','ayia-napa'),
|
|
158
|
+
jsonb_build_object(
|
|
159
|
+
'title', jsonb_build_object('sv','Ayia Napa · Stränder, nattliv & aktiviteter | SBA'),
|
|
160
|
+
'description', jsonb_build_object('sv','Upptäck Ayia Napa på Cypern — Nissi Beach, över 200 barer, partybåt, poolparty, Waterworld och Cape Greco. Allt du upplever under din bartenderutbildning.'),
|
|
161
|
+
'ogType','article',
|
|
162
|
+
'twitterCard','summary_large_image'
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
on conflict (page_key) do nothing;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
insert into public.pages (key, name, status)
|
|
2
|
+
values ('om-oss', 'Om oss', 'published')
|
|
3
|
+
on conflict (key) do nothing;
|
|
4
|
+
|
|
5
|
+
with pg as (
|
|
6
|
+
select id from public.pages where key = 'om-oss'
|
|
7
|
+
), nv as (
|
|
8
|
+
insert into public.page_versions (page_id, version_no, note, blocks)
|
|
9
|
+
select pg.id,
|
|
10
|
+
coalesce((select max(version_no) from public.page_versions where page_id = pg.id), 0) + 1,
|
|
11
|
+
'Rebuilt from barskola.nu/kontakta-oss/om-oss',
|
|
12
|
+
jsonb_build_array(
|
|
13
|
+
jsonb_build_object(
|
|
14
|
+
'id','om-hero','type','heroFull','visible',true,'props',jsonb_build_object(
|
|
15
|
+
'mediaType','image',
|
|
16
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400866656-studerande-elever-1-1920.webp',
|
|
17
|
+
'imageMobile','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400866656-studerande-elever-1-1920.webp',
|
|
18
|
+
'imageAlt', jsonb_build_object('sv','Elever studerar tillsammans på Scandinavian Bar Academy'),
|
|
19
|
+
'eyebrow', jsonb_build_object('sv','Sedan 2004'),
|
|
20
|
+
'title', jsonb_build_object('sv','Om oss'),
|
|
21
|
+
'body', jsonb_build_object('sv','Bartenders i absolut världsklass — utbildade i Medelhavet av lärare som brinner för yrket.'),
|
|
22
|
+
'primaryCta', jsonb_build_object('label', jsonb_build_object('sv','Kontakta oss'), 'href','/kontakt'),
|
|
23
|
+
'secondaryCta', jsonb_build_object('label', jsonb_build_object('sv','Se kursdatum'), 'href','/datum'),
|
|
24
|
+
'height','medium','overlay','bottom','textAlign','left','verticalAlign','bottom',
|
|
25
|
+
'textTone','light','contentWidth','normal','titleSize','xl','titleFont','serif',
|
|
26
|
+
'showScrollCue',false,'anchor','om-oss'
|
|
27
|
+
)
|
|
28
|
+
),
|
|
29
|
+
jsonb_build_object(
|
|
30
|
+
'id','om-marquee','type','imageMarquee','visible',true,'props',jsonb_build_object(
|
|
31
|
+
'eyebrow', jsonb_build_object('sv','Vår historia'),
|
|
32
|
+
'title', jsonb_build_object('sv','Världsklass sedan 2004'),
|
|
33
|
+
'lead', jsonb_build_object('sv','Scandinavian Bar Academy grundades år 2004 och har sedan dess utbildat bartenders i absolut världsklass. Genom vårt unika koncept och enastående lärare håller utbildningen högsta klass. Resultatet är elever av högsta standard som är ständigt eftertraktade på den växande arbetsmarknaden.'),
|
|
34
|
+
'items2', jsonb_build_array(
|
|
35
|
+
jsonb_build_object('id','om-t1','heading',jsonb_build_object('sv','Minnen för livet'),'body',jsonb_build_object('sv','Vi håller en professionell och hög nivå på lektionerna men strävar också efter att utbildningen och din vistelse hos oss skall ge dig minnen för livet.')),
|
|
36
|
+
jsonb_build_object('id','om-t2','heading',jsonb_build_object('sv','Upplevelsen i centrum'),'body',jsonb_build_object('sv','För oss har upplevelsen en stor betydelse. Därför har vi ett nära samarbete med Nordic Invasion – Sveriges största arrangör av ungdoms- och studentresor.'))
|
|
37
|
+
),
|
|
38
|
+
'textPosition','left','columns',2,'firstDirection','up','speed','slow',
|
|
39
|
+
'aspect','portrait','gap','md','rounded',false,'tone','white','anchor','historia',
|
|
40
|
+
'items', jsonb_build_array(
|
|
41
|
+
jsonb_build_object('id','om-m1','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400882677-elev-flair-1-1920.webp','alt',jsonb_build_object('sv','Elev tränar flair bakom baren')),
|
|
42
|
+
jsonb_build_object('id','om-m2','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/cassandra-flair-on-beach-dsc02342-1920.webp','alt',jsonb_build_object('sv','Elev flairar på stranden i Ayia Napa')),
|
|
43
|
+
jsonb_build_object('id','om-m3','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/drink-tasting-students-dsc02156-1920.webp','alt',jsonb_build_object('sv','Elever provsmakar drinkar under lektion')),
|
|
44
|
+
jsonb_build_object('id','om-m4','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/barbook-small-1-dsc00447-1920.webp','alt',jsonb_build_object('sv','Kursbok och barverktyg på bardisken')),
|
|
45
|
+
jsonb_build_object('id','om-m5','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/cocktail-dsc02143-1920.webp','alt',jsonb_build_object('sv','Färdig cocktail garnerad av elev')),
|
|
46
|
+
jsonb_build_object('id','om-m6','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400892469-studerande-elever-utomhus-1920.webp','alt',jsonb_build_object('sv','Elever studerar utomhus i solen')),
|
|
47
|
+
jsonb_build_object('id','om-m7','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400897813-magda-flair-1920.webp','alt',jsonb_build_object('sv','Elev flairar med shaker')),
|
|
48
|
+
jsonb_build_object('id','om-m8','image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400894305-elever-vid-poolen-med-bok-1920.webp','alt',jsonb_build_object('sv','Elever pluggar vid poolen'))
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
),
|
|
52
|
+
jsonb_build_object(
|
|
53
|
+
'id','om-nordic','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
54
|
+
'style','editorial','mediaSide','right','mediaRatio','50','align','center','padding','normal','tone','paper','aspect','landscape','rounded',false,
|
|
55
|
+
'mediaType','image',
|
|
56
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/barrunda-ayia-napa-1920.webp',
|
|
57
|
+
'imageAlt', jsonb_build_object('sv','Elever och resenärer på barrunda med Nordic Invasion'),
|
|
58
|
+
'eyebrow', jsonb_build_object('sv','Samarbete'),
|
|
59
|
+
'title', jsonb_build_object('sv','Tillsammans med Nordic Invasion'),
|
|
60
|
+
'body', jsonb_build_object('sv','Tillsammans med Nordic Invasion anordnar vi fester och aktiviteter som är en exklusiv förmån för deras resenärer och våra elever. Detta för att skapa härliga upplevelser utöver studierna.'),
|
|
61
|
+
'cta', jsonb_build_object('label', jsonb_build_object('sv','Upplev Ayia Napa'), 'href','/ayia-napa')
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
jsonb_build_object(
|
|
65
|
+
'id','om-mission','type','prose','visible',true,'props',jsonb_build_object(
|
|
66
|
+
'eyebrow', jsonb_build_object('sv','Mission och vision'),
|
|
67
|
+
'title', jsonb_build_object('sv','Världens mest omtyckta barskola'),
|
|
68
|
+
'lead', jsonb_build_object('sv','Scandinavian Bar Academy ska utvecklas och bli den mest framgångsrika och mest omtyckta barskolan i världen.'),
|
|
69
|
+
'tone','white','layout','two-col',
|
|
70
|
+
'items', jsonb_build_array(
|
|
71
|
+
jsonb_build_object('id','om-mv1','heading',jsonb_build_object('sv','Vår mission'),'body',jsonb_build_object('sv','Scandinavian Bar Academy ska distribuera eleverna en förstklassig bartenderutbildning och utveckla individen utifrån varje elevs förutsättningar. Scandinavian Bar Academy ska alltid vara professionellt närvarande under varje kursdag och ge eleven dess största upplevelse.')),
|
|
72
|
+
jsonb_build_object('id','om-mv2','heading',jsonb_build_object('sv','Vår vision'),'body',jsonb_build_object('sv','Genom diversifiering och respekt för sina anställda ska Scandinavian Bar Academy stimulera intern kreativitet inom företaget för att därigenom utveckla sin tjänst och alltid ligga i framkant.'))
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
),
|
|
76
|
+
jsonb_build_object(
|
|
77
|
+
'id','om-kontor','type','mediaSplit','visible',true,'props',jsonb_build_object(
|
|
78
|
+
'style','dark','mediaSide','left','mediaRatio','50','align','center','padding','normal','tone','ink','aspect','landscape','rounded',false,
|
|
79
|
+
'mediaType','image',
|
|
80
|
+
'image','https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/1785400908293-barskola-elever-bakifran-pambos-napa-rocks-1920.webp',
|
|
81
|
+
'imageAlt', jsonb_build_object('sv','Elever framför Pambos Napa Rocks i Ayia Napa'),
|
|
82
|
+
'eyebrow', jsonb_build_object('sv','Våra kontor'),
|
|
83
|
+
'title', jsonb_build_object('sv','Ayia Napa & Stockholm'),
|
|
84
|
+
'body', jsonb_build_object('sv','Scandinavian Bar Academy har kontor i Ayia Napa (Cypern) och i Stockholm (Sverige).
|
|
85
|
+
|
|
86
|
+
Du får mer än gärna besöka oss för ett personligt möte om så önskas. Kontorstiderna i Stockholm är 8–18 alla vardagar. Observera att tidsbokning krävs.'),
|
|
87
|
+
'bullets', jsonb_build_array(
|
|
88
|
+
jsonb_build_object('id','om-b1','text',jsonb_build_object('sv','Kontor i Ayia Napa, Cypern')),
|
|
89
|
+
jsonb_build_object('id','om-b2','text',jsonb_build_object('sv','Kontor i Stockholm, Sverige')),
|
|
90
|
+
jsonb_build_object('id','om-b3','text',jsonb_build_object('sv','Öppet 8–18 alla vardagar — tidsbokning krävs'))
|
|
91
|
+
),
|
|
92
|
+
'cta', jsonb_build_object('label', jsonb_build_object('sv','Våra kontaktuppgifter'), 'href','/kontakt')
|
|
93
|
+
)
|
|
94
|
+
),
|
|
95
|
+
jsonb_build_object(
|
|
96
|
+
'id','om-cta','type','ctaBanner','visible',true,'props',jsonb_build_object(
|
|
97
|
+
'tone','sea',
|
|
98
|
+
'eyebrow', jsonb_build_object('sv','Vill du veta mer?'),
|
|
99
|
+
'title', jsonb_build_object('sv','Träffa oss — i Stockholm eller Ayia Napa'),
|
|
100
|
+
'body', jsonb_build_object('sv','Vi svarar gärna på dina frågor om utbildningen, boendet och livet på plats.'),
|
|
101
|
+
'cta', jsonb_build_object('label', jsonb_build_object('sv','Kontakta oss'), 'href','/kontakt'),
|
|
102
|
+
'secondaryCta', jsonb_build_object('label', jsonb_build_object('sv','Se kursdatum'), 'href','/datum')
|
|
103
|
+
)
|
|
104
|
+
),
|
|
105
|
+
jsonb_build_object('id','om-contact','type','contactCta','visible',true,'props',jsonb_build_object())
|
|
106
|
+
)
|
|
107
|
+
from pg
|
|
108
|
+
returning id, page_id
|
|
109
|
+
)
|
|
110
|
+
insert into public.page_publications (page_id, version_id, note)
|
|
111
|
+
select nv.page_id, nv.id, 'Initial publish of /om-oss' from nv;
|
|
112
|
+
|
|
113
|
+
update public.pages p
|
|
114
|
+
set current_version_id = (
|
|
115
|
+
select v.id from public.page_versions v
|
|
116
|
+
where v.page_id = p.id order by v.version_no desc limit 1
|
|
117
|
+
), status = 'published'
|
|
118
|
+
where p.key = 'om-oss';
|
|
119
|
+
|
|
120
|
+
insert into public.page_seo (page_key, slugs, seo)
|
|
121
|
+
values (
|
|
122
|
+
'om-oss',
|
|
123
|
+
jsonb_build_object('sv','om-oss'),
|
|
124
|
+
jsonb_build_object(
|
|
125
|
+
'title', jsonb_build_object('sv','Om oss · Scandinavian Bar Academy sedan 2004'),
|
|
126
|
+
'description', jsonb_build_object('sv','Scandinavian Bar Academy grundades 2004 och utbildar bartenders i världsklass. Läs om vår historia, mission och vision samt våra kontor i Ayia Napa och Stockholm.'),
|
|
127
|
+
'ogType','article',
|
|
128
|
+
'twitterCard','summary_large_image'
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
on conflict (page_key) do nothing;
|
|
132
|
+
|
|
133
|
+
insert into public.redirects (from_path, to_path, status)
|
|
134
|
+
values ('/kontakta-oss/om-oss', '/om-oss', 301)
|
|
135
|
+
on conflict (from_path) do update set to_path = excluded.to_path, status = excluded.status;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
update page_versions v set blocks = (
|
|
2
|
+
select jsonb_agg(
|
|
3
|
+
case when b->>'id' = 'om-mission' then
|
|
4
|
+
jsonb_build_object('id','om-mission','type','imageMarquee','props', jsonb_build_object(
|
|
5
|
+
'eyebrow', b->'props'->'eyebrow','title', b->'props'->'title','lead', b->'props'->'lead',
|
|
6
|
+
'items2', b->'props'->'items','items', $J$[{"id": "om-mm1", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/student-flair-dsc01358-1920.webp", "alt": {"sv": "Elev tränar flair i baren"}}, {"id": "om-mm2", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/students-study-at-beach-dsc01566-1920.webp", "alt": {"sv": "Elever pluggar på stranden"}}, {"id": "om-mm3", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/toni-and-students-1-dsc00166jpg-1920.webp", "alt": {"sv": "Lärare och elever under lektion"}}, {"id": "om-mm4", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/students-and-cocktails-dsc02105-1920.webp", "alt": {"sv": "Elever med färdiga cocktails"}}, {"id": "om-mm5", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/student-pour-1-dsc00573-1920.webp", "alt": {"sv": "Elev häller upp en drink"}}, {"id": "om-mm6", "image": "https://hcfwpwfzbcapfdemvsnh.supabase.co/storage/v1/object/public/media/ronja-and-sba-flag-1-dsc00188-1920.webp", "alt": {"sv": "Elev med SBA-flaggan"}}]$J$::jsonb,
|
|
7
|
+
'tone', coalesce(b->'props'->'tone','"white"'::jsonb),'columns', to_jsonb(2),
|
|
8
|
+
'aspect','"portrait"'::jsonb,'gap','"md"'::jsonb,'speed','"slow"'::jsonb,
|
|
9
|
+
'firstDirection','"up"'::jsonb,'rounded','false'::jsonb,'textPosition','"right"'::jsonb,
|
|
10
|
+
'anchor', coalesce(b->'props'->'anchor','""'::jsonb)))
|
|
11
|
+
else b end order by ord)
|
|
12
|
+
from jsonb_array_elements(v.blocks) with ordinality t(b,ord))
|
|
13
|
+
from pages p where p.key='om-oss' and v.id=p.current_version_id;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
ALTER TABLE public.pages
|
|
2
|
+
ADD COLUMN IF NOT EXISTS deleted_at timestamptz,
|
|
3
|
+
ADD COLUMN IF NOT EXISTS deleted_by uuid,
|
|
4
|
+
ADD COLUMN IF NOT EXISTS deleted_by_email text;
|
|
5
|
+
|
|
6
|
+
CREATE INDEX IF NOT EXISTS pages_deleted_at_idx ON public.pages (deleted_at);
|
|
7
|
+
|
|
8
|
+
DROP POLICY IF EXISTS "Admins delete pages" ON public.pages;
|
|
9
|
+
CREATE POLICY "Superadmins delete pages"
|
|
10
|
+
ON public.pages
|
|
11
|
+
FOR DELETE
|
|
12
|
+
TO authenticated
|
|
13
|
+
USING (has_role(auth.uid(), 'superadmin'::app_role) AND is_system = false);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ALTER TABLE public.forms
|
|
2
|
+
ADD COLUMN IF NOT EXISTS deleted_at timestamptz,
|
|
3
|
+
ADD COLUMN IF NOT EXISTS deleted_by uuid,
|
|
4
|
+
ADD COLUMN IF NOT EXISTS deleted_by_email text;
|
|
5
|
+
|
|
6
|
+
DROP POLICY IF EXISTS "forms admin delete" ON public.forms;
|
|
7
|
+
CREATE POLICY "forms superadmin delete" ON public.forms
|
|
8
|
+
FOR DELETE TO authenticated
|
|
9
|
+
USING (public.has_role(auth.uid(), 'superadmin'));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS public.media_meta (
|
|
2
|
+
path text PRIMARY KEY,
|
|
3
|
+
alt jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
4
|
+
title jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
5
|
+
description jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
6
|
+
credit text NOT NULL DEFAULT '',
|
|
7
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
8
|
+
updated_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
9
|
+
updated_by uuid
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON public.media_meta TO authenticated;
|
|
13
|
+
GRANT ALL ON public.media_meta TO service_role;
|
|
14
|
+
|
|
15
|
+
ALTER TABLE public.media_meta ENABLE ROW LEVEL SECURITY;
|
|
16
|
+
|
|
17
|
+
CREATE POLICY "Staff read media_meta" ON public.media_meta
|
|
18
|
+
FOR SELECT TO authenticated USING (public.is_staff(auth.uid()));
|
|
19
|
+
CREATE POLICY "Staff insert media_meta" ON public.media_meta
|
|
20
|
+
FOR INSERT TO authenticated WITH CHECK (public.is_staff(auth.uid()));
|
|
21
|
+
CREATE POLICY "Staff update media_meta" ON public.media_meta
|
|
22
|
+
FOR UPDATE TO authenticated USING (public.is_staff(auth.uid())) WITH CHECK (public.is_staff(auth.uid()));
|
|
23
|
+
CREATE POLICY "Staff delete media_meta" ON public.media_meta
|
|
24
|
+
FOR DELETE TO authenticated USING (public.is_staff(auth.uid()));
|
|
25
|
+
|
|
26
|
+
CREATE OR REPLACE FUNCTION public.touch_media_meta()
|
|
27
|
+
RETURNS trigger
|
|
28
|
+
LANGUAGE plpgsql
|
|
29
|
+
SET search_path = public
|
|
30
|
+
AS $$
|
|
31
|
+
BEGIN
|
|
32
|
+
NEW.updated_at = now();
|
|
33
|
+
NEW.updated_by = auth.uid();
|
|
34
|
+
RETURN NEW;
|
|
35
|
+
END;
|
|
36
|
+
$$;
|
|
37
|
+
|
|
38
|
+
CREATE TRIGGER touch_media_meta_before_update
|
|
39
|
+
BEFORE UPDATE ON public.media_meta
|
|
40
|
+
FOR EACH ROW EXECUTE FUNCTION public.touch_media_meta();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
UPDATE public.global_sections SET current_version_id = NULL WHERE key LIKE 'tmp-verify%';
|
|
2
|
+
DELETE FROM public.global_section_versions WHERE section_id IN (SELECT id FROM public.global_sections WHERE key LIKE 'tmp-verify%');
|
|
3
|
+
DELETE FROM public.global_sections WHERE key LIKE 'tmp-verify%';
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
-- 1. Portability layer -------------------------------------------------
|
|
2
|
+
CREATE SCHEMA IF NOT EXISTS app;
|
|
3
|
+
GRANT USAGE ON SCHEMA app TO anon, authenticated, service_role;
|
|
4
|
+
|
|
5
|
+
-- Single point of indirection. On a vanilla Postgres install this function is
|
|
6
|
+
-- replaced by db/bootstrap.sql to read a session setting instead.
|
|
7
|
+
CREATE OR REPLACE FUNCTION app.current_user_id()
|
|
8
|
+
RETURNS uuid
|
|
9
|
+
LANGUAGE sql
|
|
10
|
+
STABLE
|
|
11
|
+
SECURITY DEFINER
|
|
12
|
+
SET search_path = auth, public, pg_temp
|
|
13
|
+
AS $$
|
|
14
|
+
SELECT auth.uid()
|
|
15
|
+
$$;
|
|
16
|
+
|
|
17
|
+
CREATE OR REPLACE FUNCTION app.has_role(_user_id uuid, _role public.app_role)
|
|
18
|
+
RETURNS boolean
|
|
19
|
+
LANGUAGE sql
|
|
20
|
+
STABLE
|
|
21
|
+
SECURITY DEFINER
|
|
22
|
+
SET search_path = public
|
|
23
|
+
AS $$
|
|
24
|
+
SELECT EXISTS (
|
|
25
|
+
SELECT 1 FROM public.user_roles
|
|
26
|
+
WHERE user_id = _user_id
|
|
27
|
+
AND (role = _role OR (role = 'superadmin'::public.app_role AND _role <> 'superadmin'::public.app_role))
|
|
28
|
+
)
|
|
29
|
+
$$;
|
|
30
|
+
|
|
31
|
+
CREATE OR REPLACE FUNCTION app.is_staff(_user_id uuid)
|
|
32
|
+
RETURNS boolean
|
|
33
|
+
LANGUAGE sql
|
|
34
|
+
STABLE
|
|
35
|
+
SECURITY DEFINER
|
|
36
|
+
SET search_path = public
|
|
37
|
+
AS $$
|
|
38
|
+
SELECT EXISTS (
|
|
39
|
+
SELECT 1 FROM public.user_roles
|
|
40
|
+
WHERE user_id = _user_id AND role IN ('admin','superadmin')
|
|
41
|
+
)
|
|
42
|
+
$$;
|
|
43
|
+
|
|
44
|
+
GRANT EXECUTE ON FUNCTION app.current_user_id() TO anon, authenticated, service_role;
|
|
45
|
+
GRANT EXECUTE ON FUNCTION app.has_role(uuid, public.app_role) TO anon, authenticated, service_role;
|
|
46
|
+
GRANT EXECUTE ON FUNCTION app.is_staff(uuid) TO anon, authenticated, service_role;
|
|
47
|
+
|
|
48
|
+
-- Existing public helpers become thin wrappers so application code is unchanged.
|
|
49
|
+
CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role public.app_role)
|
|
50
|
+
RETURNS boolean
|
|
51
|
+
LANGUAGE sql
|
|
52
|
+
STABLE
|
|
53
|
+
SECURITY DEFINER
|
|
54
|
+
SET search_path = public
|
|
55
|
+
AS $$
|
|
56
|
+
SELECT app.has_role(_user_id, _role)
|
|
57
|
+
$$;
|
|
58
|
+
|
|
59
|
+
CREATE OR REPLACE FUNCTION public.is_staff(_user_id uuid)
|
|
60
|
+
RETURNS boolean
|
|
61
|
+
LANGUAGE sql
|
|
62
|
+
STABLE
|
|
63
|
+
SECURITY DEFINER
|
|
64
|
+
SET search_path = public
|
|
65
|
+
AS $$
|
|
66
|
+
SELECT app.is_staff(_user_id)
|
|
67
|
+
$$;
|
|
68
|
+
|
|
69
|
+
-- 2. Own the user table -------------------------------------------------
|
|
70
|
+
CREATE TABLE IF NOT EXISTS public.app_users (
|
|
71
|
+
id uuid PRIMARY KEY,
|
|
72
|
+
email text,
|
|
73
|
+
display_name text,
|
|
74
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
75
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
76
|
+
last_seen_at timestamptz
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
GRANT SELECT ON public.app_users TO authenticated;
|
|
80
|
+
GRANT ALL ON public.app_users TO service_role;
|
|
81
|
+
ALTER TABLE public.app_users ENABLE ROW LEVEL SECURITY;
|
|
82
|
+
|
|
83
|
+
DROP POLICY IF EXISTS "Users can view themselves" ON public.app_users;
|
|
84
|
+
CREATE POLICY "Users can view themselves" ON public.app_users
|
|
85
|
+
FOR SELECT TO authenticated USING (id = app.current_user_id());
|
|
86
|
+
|
|
87
|
+
DROP POLICY IF EXISTS "Staff view app_users" ON public.app_users;
|
|
88
|
+
CREATE POLICY "Staff view app_users" ON public.app_users
|
|
89
|
+
FOR SELECT TO authenticated USING (public.is_staff(app.current_user_id()));
|
|
90
|
+
|
|
91
|
+
CREATE OR REPLACE FUNCTION public.touch_app_users()
|
|
92
|
+
RETURNS trigger LANGUAGE plpgsql SET search_path = public AS $$
|
|
93
|
+
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$;
|
|
94
|
+
|
|
95
|
+
DROP TRIGGER IF EXISTS touch_app_users ON public.app_users;
|
|
96
|
+
CREATE TRIGGER touch_app_users BEFORE UPDATE ON public.app_users
|
|
97
|
+
FOR EACH ROW EXECUTE FUNCTION public.touch_app_users();
|
|
98
|
+
|
|
99
|
+
-- Backfill every existing account.
|
|
100
|
+
INSERT INTO public.app_users (id, email, display_name, created_at)
|
|
101
|
+
SELECT u.id,
|
|
102
|
+
u.email,
|
|
103
|
+
COALESCE(u.raw_user_meta_data->>'full_name', u.raw_user_meta_data->>'name'),
|
|
104
|
+
u.created_at
|
|
105
|
+
FROM auth.users u
|
|
106
|
+
ON CONFLICT (id) DO NOTHING;
|
|
107
|
+
|
|
108
|
+
-- Keeps app_users complete whenever a role is granted, so the foreign key
|
|
109
|
+
-- below can never block a legitimate insert.
|
|
110
|
+
CREATE OR REPLACE FUNCTION public.ensure_app_user()
|
|
111
|
+
RETURNS trigger
|
|
112
|
+
LANGUAGE plpgsql
|
|
113
|
+
SECURITY DEFINER
|
|
114
|
+
SET search_path = public, auth, pg_temp
|
|
115
|
+
AS $$
|
|
116
|
+
BEGIN
|
|
117
|
+
INSERT INTO public.app_users (id, email, display_name, created_at)
|
|
118
|
+
SELECT u.id,
|
|
119
|
+
u.email,
|
|
120
|
+
COALESCE(u.raw_user_meta_data->>'full_name', u.raw_user_meta_data->>'name'),
|
|
121
|
+
u.created_at
|
|
122
|
+
FROM auth.users u
|
|
123
|
+
WHERE u.id = NEW.user_id
|
|
124
|
+
ON CONFLICT (id) DO NOTHING;
|
|
125
|
+
|
|
126
|
+
INSERT INTO public.app_users (id) VALUES (NEW.user_id)
|
|
127
|
+
ON CONFLICT (id) DO NOTHING;
|
|
128
|
+
|
|
129
|
+
RETURN NEW;
|
|
130
|
+
END;
|
|
131
|
+
$$;
|
|
132
|
+
|
|
133
|
+
DROP TRIGGER IF EXISTS ensure_app_user_before_role ON public.user_roles;
|
|
134
|
+
CREATE TRIGGER ensure_app_user_before_role
|
|
135
|
+
BEFORE INSERT ON public.user_roles
|
|
136
|
+
FOR EACH ROW EXECUTE FUNCTION public.ensure_app_user();
|
|
137
|
+
|
|
138
|
+
-- Also record the account when the initial admin trigger fires.
|
|
139
|
+
CREATE OR REPLACE FUNCTION public.grant_initial_admin()
|
|
140
|
+
RETURNS trigger
|
|
141
|
+
LANGUAGE plpgsql
|
|
142
|
+
SECURITY DEFINER
|
|
143
|
+
SET search_path = public
|
|
144
|
+
AS $$
|
|
145
|
+
BEGIN
|
|
146
|
+
IF NEW.email_confirmed_at IS NOT NULL THEN
|
|
147
|
+
INSERT INTO public.app_users (id, email, display_name, created_at)
|
|
148
|
+
VALUES (NEW.id, NEW.email,
|
|
149
|
+
COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'),
|
|
150
|
+
NEW.created_at)
|
|
151
|
+
ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email;
|
|
152
|
+
END IF;
|
|
153
|
+
|
|
154
|
+
IF NEW.email_confirmed_at IS NOT NULL
|
|
155
|
+
AND lower(NEW.email) = 'byggis@gmail.com' THEN
|
|
156
|
+
INSERT INTO public.user_roles (user_id, role) VALUES (NEW.id, 'admin')
|
|
157
|
+
ON CONFLICT (user_id, role) DO NOTHING;
|
|
158
|
+
INSERT INTO public.user_roles (user_id, role) VALUES (NEW.id, 'superadmin')
|
|
159
|
+
ON CONFLICT (user_id, role) DO NOTHING;
|
|
160
|
+
END IF;
|
|
161
|
+
RETURN NEW;
|
|
162
|
+
END;
|
|
163
|
+
$$;
|
|
164
|
+
|
|
165
|
+
-- Re-point the two foreign keys away from the hosted auth table.
|
|
166
|
+
ALTER TABLE public.user_roles DROP CONSTRAINT IF EXISTS user_roles_user_id_fkey;
|
|
167
|
+
ALTER TABLE public.user_roles
|
|
168
|
+
ADD CONSTRAINT user_roles_user_id_fkey
|
|
169
|
+
FOREIGN KEY (user_id) REFERENCES public.app_users(id) ON DELETE CASCADE;
|
|
170
|
+
|
|
171
|
+
ALTER TABLE public.content_sections DROP CONSTRAINT IF EXISTS content_sections_updated_by_fkey;
|
|
172
|
+
ALTER TABLE public.content_sections
|
|
173
|
+
ADD CONSTRAINT content_sections_updated_by_fkey
|
|
174
|
+
FOREIGN KEY (updated_by) REFERENCES public.app_users(id) ON DELETE SET NULL;
|
|
175
|
+
|
|
176
|
+
-- 3. Route every policy through the portability layer -------------------
|
|
177
|
+
DO $do$
|
|
178
|
+
DECLARE
|
|
179
|
+
p record;
|
|
180
|
+
stmt text;
|
|
181
|
+
BEGIN
|
|
182
|
+
FOR p IN
|
|
183
|
+
SELECT tablename, policyname, permissive, cmd, roles, qual, with_check
|
|
184
|
+
FROM pg_policies
|
|
185
|
+
WHERE schemaname = 'public'
|
|
186
|
+
AND (qual LIKE '%auth.uid()%' OR with_check LIKE '%auth.uid()%')
|
|
187
|
+
LOOP
|
|
188
|
+
stmt := format(
|
|
189
|
+
'DROP POLICY %I ON public.%I',
|
|
190
|
+
p.policyname, p.tablename
|
|
191
|
+
);
|
|
192
|
+
EXECUTE stmt;
|
|
193
|
+
|
|
194
|
+
stmt := format(
|
|
195
|
+
'CREATE POLICY %I ON public.%I AS %s FOR %s TO %s',
|
|
196
|
+
p.policyname, p.tablename, p.permissive, p.cmd,
|
|
197
|
+
array_to_string(p.roles, ', ')
|
|
198
|
+
);
|
|
199
|
+
IF p.qual IS NOT NULL THEN
|
|
200
|
+
stmt := stmt || ' USING (' || replace(p.qual, 'auth.uid()', 'app.current_user_id()') || ')';
|
|
201
|
+
END IF;
|
|
202
|
+
IF p.with_check IS NOT NULL THEN
|
|
203
|
+
stmt := stmt || ' WITH CHECK (' || replace(p.with_check, 'auth.uid()', 'app.current_user_id()') || ')';
|
|
204
|
+
END IF;
|
|
205
|
+
EXECUTE stmt;
|
|
206
|
+
END LOOP;
|
|
207
|
+
END
|
|
208
|
+
$do$;
|
|
209
|
+
|
|
210
|
+
-- 4. Triggers use the portability layer too ------------------------------
|
|
211
|
+
DO $do$
|
|
212
|
+
DECLARE
|
|
213
|
+
f record;
|
|
214
|
+
BEGIN
|
|
215
|
+
FOR f IN
|
|
216
|
+
SELECT p.oid, p.proname, pg_get_functiondef(p.oid) AS def
|
|
217
|
+
FROM pg_proc p
|
|
218
|
+
WHERE p.pronamespace = 'public'::regnamespace
|
|
219
|
+
AND p.proname LIKE 'touch\_%'
|
|
220
|
+
AND pg_get_functiondef(p.oid) LIKE '%auth.uid()%'
|
|
221
|
+
LOOP
|
|
222
|
+
EXECUTE replace(f.def, 'auth.uid()', 'app.current_user_id()');
|
|
223
|
+
END LOOP;
|
|
224
|
+
END
|
|
225
|
+
$do$;
|