feat(branding): add global dynamic theme system and fix 2fa mail

This commit is contained in:
DanielS
2026-08-14 15:57:29 +02:00
parent 009b167cbd
commit 9ecdc645e8
22 changed files with 1764 additions and 313 deletions

View File

@@ -1,4 +1,3 @@
-- Migration: Update user creation trigger function to not assign admin role to info@hephex.de automatically
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN

View File

@@ -1,30 +1,63 @@
-- Migration: Secure User Roles from Self-Escalation
-- Purpose: Prevent users from updating their own roles to 'admin' using RLS / Triggers.
-- Purpose: Prevent users from updating their own roles to 'admin' using RLS / Triggers, while granting full access to service_role.
-- Create helper function to check admin role bypassing RLS (SECURITY DEFINER)
CREATE OR REPLACE FUNCTION public.is_admin(user_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM public.users
WHERE id = user_id AND role = 'admin'
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Ensure RLS is enabled on users
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
-- Grant privileges to PostgREST roles
GRANT ALL ON TABLE public.users TO service_role;
GRANT ALL ON TABLE public.users TO authenticated;
GRANT SELECT ON TABLE public.users TO anon;
GRANT ALL ON TABLE public.users TO postgres;
-- Service role policy
DROP POLICY IF EXISTS "service_role_all_users" ON public.users;
CREATE POLICY "service_role_all_users" ON public.users
FOR ALL
TO service_role
USING (true)
WITH CHECK (true);
-- Policy to allow users to view their own records
DROP POLICY IF EXISTS select_own_user ON public.users;
CREATE POLICY select_own_user ON public.users
FOR SELECT
TO authenticated
USING (auth.uid() = id);
-- Policy to allow admins to view all users
DROP POLICY IF EXISTS select_all_users_for_admin ON public.users;
CREATE POLICY select_all_users_for_admin ON public.users
FOR SELECT
TO authenticated
USING (
(SELECT role FROM public.users WHERE id = auth.uid()) = 'admin'
);
USING (public.is_admin(auth.uid()));
-- Policy to allow admins to update users
DROP POLICY IF EXISTS update_users_for_admin ON public.users;
CREATE POLICY update_users_for_admin ON public.users
FOR UPDATE
TO authenticated
USING (public.is_admin(auth.uid()))
WITH CHECK (public.is_admin(auth.uid()));
-- Trigger to prevent any role updates to 'admin' from unauthorized users
CREATE OR REPLACE FUNCTION check_user_role_escalation()
RETURNS TRIGGER AS $$
BEGIN
-- Only allow changes to the role column if executed by the service_role
-- Allow changes to the role column if executed by administrative DB roles or service_role JWT
IF (TG_OP = 'UPDATE' AND OLD.role IS DISTINCT FROM NEW.role) OR (TG_OP = 'INSERT') THEN
IF current_setting('role', true) <> 'service_role' THEN
IF current_setting('request.jwt.claim.role', true) <> 'service_role'
AND current_setting('role', true) NOT IN ('service_role', 'supabase_admin', 'postgres') THEN
-- Partners cannot upgrade themselves or others to admin
IF NEW.role = 'admin' THEN
RAISE EXCEPTION 'Unberechtigtes Rollen-Upgrade verweigert.';

View File

@@ -1,26 +1,28 @@
-- Migration: Restrict settings table write access to admins only
-- Previously any authenticated user could write to settings (including licserver_api_key).
-- This fixes the RLS policy to only allow admins to write.
-- Migration: Restrict settings table write access to admins only and allow service_role full access
ALTER TABLE public.settings ENABLE ROW LEVEL SECURITY;
-- Grant privileges to PostgREST roles
GRANT ALL ON TABLE public.settings TO service_role;
GRANT ALL ON TABLE public.settings TO authenticated;
GRANT SELECT ON TABLE public.settings TO anon;
GRANT ALL ON TABLE public.settings TO postgres;
-- Service role policy
DROP POLICY IF EXISTS "service_role_all_settings" ON public.settings;
CREATE POLICY "service_role_all_settings" ON public.settings
FOR ALL
TO service_role
USING (true)
WITH CHECK (true);
DROP POLICY IF EXISTS "Allow authenticated write on settings" ON public.settings;
DROP POLICY IF EXISTS "Only admins can write settings" ON public.settings;
-- Admins can write all settings
CREATE POLICY "Only admins can write settings" ON public.settings
FOR ALL
USING (
EXISTS (
SELECT 1 FROM public.users
WHERE id = auth.uid() AND role = 'admin'
)
)
WITH CHECK (
EXISTS (
SELECT 1 FROM public.users
WHERE id = auth.uid() AND role = 'admin'
)
);
-- All authenticated users can still READ settings (needed for proxy routes to load licserver config)
-- The READ policy remains: "Allow authenticated read on settings"
TO authenticated
USING (public.is_admin(auth.uid()))
WITH CHECK (public.is_admin(auth.uid()));
NOTIFY pgrst, 'reload schema';

View File

@@ -0,0 +1,17 @@
-- Migration: Add Branding & Company Settings to public.settings
-- Stores company details, addresses, and chosen color scheme.
ALTER TABLE public.settings
ADD COLUMN IF NOT EXISTS company_name TEXT,
ADD COLUMN IF NOT EXISTS street TEXT,
ADD COLUMN IF NOT EXISTS zip TEXT,
ADD COLUMN IF NOT EXISTS city TEXT,
ADD COLUMN IF NOT EXISTS billing_street TEXT,
ADD COLUMN IF NOT EXISTS billing_zip TEXT,
ADD COLUMN IF NOT EXISTS billing_city TEXT,
ADD COLUMN IF NOT EXISTS same_billing_address BOOLEAN DEFAULT true,
ADD COLUMN IF NOT EXISTS logo_url TEXT,
ADD COLUMN IF NOT EXISTS developer_footer TEXT DEFAULT 'B2B Shop made by hephex',
ADD COLUMN IF NOT EXISTS color_scheme TEXT DEFAULT 'modern_blue',
ADD COLUMN IF NOT EXISTS primary_color TEXT DEFAULT '#2563eb',
ADD COLUMN IF NOT EXISTS accent_color TEXT DEFAULT '#38bdf8';

View File

@@ -2,15 +2,15 @@
INSERT INTO public.products (id, name, description, base_price, tax_rate, billing_interval, show_in_abo, show_in_kauf)
VALUES
('d1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'CASPOS Cloud', 'Die modulare Cloud-Lösung für Ihren Einzelhandel.', 49.00, 19.00, 'monthly', true, true),
('d2a2a2a2-a2a2-a2a2-a2a2-a2a2-a2a2-a2a2a2a2', 'CASPOS Gastro', 'Spezialisiert auf Gastronomie mit Tischplan und Funkbonieren.', 79.00, 19.00, 'monthly', true, true),
('prod-poscloud-fee', 'POS Cloud Grundgebühr', 'Monatliche Grundgebühr für die POS Cloud Nutzung.', 19.00, 19.00, 'monthly', false, false);
('d2a2a2a2-a2a2-a2a2-a2a2-a2a2a2a2a2a2', 'CASPOS Gastro', 'Spezialisiert auf Gastronomie mit Tischplan und Funkbonieren.', 79.00, 19.00, 'monthly', true, true),
('d3a3a3a3-a3a3-a3a3-a3a3-a3a3a3a3a3a3', 'POS Cloud Grundgebühr', 'Monatliche Grundgebühr für die POS Cloud Nutzung.', 19.00, 19.00, 'monthly', false, false);
-- Seed Modules for CASPOS Cloud
INSERT INTO public.product_modules (id, product_id, name, description, price, requirements, exclusions)
VALUES
('m1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Bestandsführung Pro', 'Erweiterte Lagerverwaltung.', 15.00, '{}', '{}'),
('m2a2a2a2-a2a2-a2a2-a2a2-a2a2-a2a2-a2a2a2a2', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'DATEV Export', 'Direkte Schnittstelle zum Steuerberater.', 10.00, '{}', '{}'),
('m3a3a3a3-a3a3-a3a3-a3a3-a3a3-a3a3-a3a3-a3a3', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Filialverwaltung', 'Zentrale Steuerung mehrerer Standorte.', 25.00, '{"m1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"}', '{}'),
('m4a4a4a4-a4a4-a4a4-a4a4-a4a4-a4a4-a4a4-a4a4', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Small Business Modus', 'Reduzierter Funktionsumfang für Kleinunternehmer.', 0.00, '{}', '{"m1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1", "m3a3a3a3-a3a3-a3a3-a3a3-a3a3-a3a3-a3a3-a3a3"}'),
('m-poscloud-cloud', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Schnittstelle POS Cloud', 'Anbindung an die POS Cloud.', 0.00, '{}', '{}'),
('m-poscloud-gastro', 'd2a2a2a2-a2a2-a2a2-a2a2-a2a2-a2a2-a2a2a2a2', 'Schnittstelle POS Cloud', 'Anbindung an die POS Cloud.', 0.00, '{}', '{}');
('e1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Bestandsführung Pro', 'Erweiterte Lagerverwaltung.', 15.00, '{}', '{}'),
('e2a2a2a2-a2a2-a2a2-a2a2-a2a2a2a2a2a2', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'DATEV Export', 'Direkte Schnittstelle zum Steuerberater.', 10.00, '{}', '{}'),
('e3a3a3a3-a3a3-a3a3-a3a3-a3a3a3a3a3a3', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Filialverwaltung', 'Zentrale Steuerung mehrerer Standorte.', 25.00, '{"e1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1"}', '{}'),
('e4a4a4a4-a4a4-a4a4-a4a4-a4a4a4a4a4a4', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Small Business Modus', 'Reduzierter Funktionsumfang für Kleinunternehmer.', 0.00, '{}', '{"e1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1", "e3a3a3a3-a3a3-a3a3-a3a3-a3a3a3a3a3a3"}'),
('e5a5a5a5-a5a5-a5a5-a5a5-a5a5a5a5a5a5', 'd1a1a1a1-a1a1-a1a1-a1a1-a1a1a1a1a1a1', 'Schnittstelle POS Cloud', 'Anbindung an die POS Cloud.', 0.00, '{}', '{}'),
('e6a6a6a6-a6a6-a6a6-a6a6-a6a6a6a6a6a6', 'd2a2a2a2-a2a2-a2a2-a2a2-a2a2a2a2a2a2', 'Schnittstelle POS Cloud', 'Anbindung an die POS Cloud.', 0.00, '{}', '{}');