diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index f347d25..6ba0e35 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -186,15 +186,42 @@ export type Database = { } Relationships: [] } + user_roles: { + Row: { + created_at: string + id: string + role: Database["public"]["Enums"]["app_role"] + user_id: string + } + Insert: { + created_at?: string + id?: string + role: Database["public"]["Enums"]["app_role"] + user_id: string + } + Update: { + created_at?: string + id?: string + role?: Database["public"]["Enums"]["app_role"] + user_id?: string + } + Relationships: [] + } } Views: { [_ in never]: never } Functions: { - [_ in never]: never + has_role: { + Args: { + _role: Database["public"]["Enums"]["app_role"] + _user_id: string + } + Returns: boolean + } } Enums: { - [_ in never]: never + app_role: "admin" | "user" } CompositeTypes: { [_ in never]: never @@ -321,6 +348,8 @@ export type CompositeTypes< export const Constants = { public: { - Enums: {}, + Enums: { + app_role: ["admin", "user"], + }, }, } as const diff --git a/src/pages/Admin.tsx b/src/pages/Admin.tsx index 3cf9fb3..ad0ee15 100644 --- a/src/pages/Admin.tsx +++ b/src/pages/Admin.tsx @@ -8,50 +8,52 @@ import { OrdersTable } from "@/components/admin/OrdersTable"; import { CustomersTable } from "@/components/admin/CustomersTable"; import { PaymentsTable } from "@/components/admin/PaymentsTable"; import { LeadsGenerator } from "@/components/admin/LeadsGenerator"; -import { Package, Users, CreditCard, LogOut, ArrowLeft, Sparkles } from "lucide-react"; +import AuthDialog from "@/components/AuthDialog"; +import { Package, Users, CreditCard, LogOut, ArrowLeft, Sparkles, Lock } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; +type AuthState = "loading" | "signed_out" | "not_admin" | "admin"; + export default function Admin() { const navigate = useNavigate(); - const [loading, setLoading] = useState(true); - const [isAuthenticated, setIsAuthenticated] = useState(false); + const [state, setState] = useState("loading"); + const [authOpen, setAuthOpen] = useState(false); + const [authMode, setAuthMode] = useState<"login" | "signup">("login"); useEffect(() => { - const checkAuth = async () => { - const { data: { session } } = await supabase.auth.getSession(); - if (!session) { - navigate("/"); + const check = async (session: any) => { + if (!session?.user) { + setState("signed_out"); return; } - setIsAuthenticated(true); - setLoading(false); + const { data, error } = await supabase + .from("user_roles") + .select("role") + .eq("user_id", session.user.id) + .eq("role", "admin") + .maybeSingle(); + if (error) console.error("Role check failed:", error); + setState(data ? "admin" : "not_admin"); }; - const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { - if (!session) { - navigate("/"); - } + const { data: { subscription } } = supabase.auth.onAuthStateChange((_e, session) => { + check(session); }); - - checkAuth(); - + supabase.auth.getSession().then(({ data }) => check(data.session)); return () => subscription.unsubscribe(); - }, [navigate]); + }, []); const handleLogout = async () => { await supabase.auth.signOut(); - navigate("/"); }; - if (loading) { + if (state === "loading") { return (
- {[...Array(4)].map((_, i) => ( - - ))} + {[...Array(4)].map((_, i) => )}
@@ -59,8 +61,43 @@ export default function Admin() { ); } - if (!isAuthenticated) { - return null; + if (state === "signed_out") { + return ( + <> +
+
+ +

Admin-inloggning krävs

+

+ Denna sida är privat. Logga in med ditt admin-konto för att se ordrar och annan intern information. +

+
+ + +
+
+
+ + + ); + } + + if (state === "not_admin") { + return ( +
+
+ +

Ingen behörighet

+

+ Ditt konto har inte tillgång till admin-sidan. Endast ägaren kan se den här sidan. +

+
+ + +
+
+
+ ); } return ( diff --git a/supabase/migrations/20260711135235_dc1842ae-a480-4bfe-8b1e-f88b475b421e.sql b/supabase/migrations/20260711135235_dc1842ae-a480-4bfe-8b1e-f88b475b421e.sql new file mode 100644 index 0000000..23dd2f5 --- /dev/null +++ b/supabase/migrations/20260711135235_dc1842ae-a480-4bfe-8b1e-f88b475b421e.sql @@ -0,0 +1,62 @@ + +-- Role enum +DO $$ BEGIN + CREATE TYPE public.app_role AS ENUM ('admin', 'user'); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- user_roles table +CREATE TABLE IF NOT EXISTS public.user_roles ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + role public.app_role NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (user_id, role) +); + +GRANT SELECT ON public.user_roles TO authenticated; +GRANT ALL ON public.user_roles TO service_role; + +ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Users can view own roles" ON public.user_roles; +CREATE POLICY "Users can view own roles" + ON public.user_roles FOR SELECT + TO authenticated + USING (auth.uid() = user_id); + +-- Security definer role check +CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role public.app_role) +RETURNS boolean +LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public +AS $$ + SELECT EXISTS ( + SELECT 1 FROM public.user_roles + WHERE user_id = _user_id AND role = _role + ) +$$; + +-- Backfill admin for existing owner +INSERT INTO public.user_roles (user_id, role) +SELECT id, 'admin'::public.app_role +FROM auth.users +WHERE lower(email) = 'tiffanysvson@gmail.com' +ON CONFLICT (user_id, role) DO NOTHING; + +-- Auto-grant admin on future signup with owner email +CREATE OR REPLACE FUNCTION public.handle_new_user() +RETURNS trigger +LANGUAGE plpgsql SECURITY DEFINER SET search_path = public +AS $$ +BEGIN + INSERT INTO public.profiles (user_id) VALUES (NEW.id) + ON CONFLICT DO NOTHING; + + IF lower(NEW.email) = 'tiffanysvson@gmail.com' THEN + INSERT INTO public.user_roles (user_id, role) + VALUES (NEW.id, 'admin') + ON CONFLICT (user_id, role) DO NOTHING; + END IF; + + RETURN NEW; +END; +$$; diff --git a/supabase/migrations/20260711135248_69ae14dc-d0e0-43eb-ae70-f313f8a2a4e0.sql b/supabase/migrations/20260711135248_69ae14dc-d0e0-43eb-ae70-f313f8a2a4e0.sql new file mode 100644 index 0000000..9224bdc --- /dev/null +++ b/supabase/migrations/20260711135248_69ae14dc-d0e0-43eb-ae70-f313f8a2a4e0.sql @@ -0,0 +1,7 @@ + +REVOKE ALL ON FUNCTION public.has_role(uuid, public.app_role) FROM PUBLIC, anon; +GRANT EXECUTE ON FUNCTION public.has_role(uuid, public.app_role) TO authenticated, service_role; + +REVOKE ALL ON FUNCTION public.handle_new_user() FROM PUBLIC, anon, authenticated; + +REVOKE ALL ON FUNCTION public.update_updated_at_column() FROM PUBLIC, anon, authenticated; diff --git a/supabase/migrations/20260711135259_21be1d79-4fce-4a98-8883-62dd781c72d7.sql b/supabase/migrations/20260711135259_21be1d79-4fce-4a98-8883-62dd781c72d7.sql new file mode 100644 index 0000000..3373370 --- /dev/null +++ b/supabase/migrations/20260711135259_21be1d79-4fce-4a98-8883-62dd781c72d7.sql @@ -0,0 +1,27 @@ + +-- Orders: admin-only reads +DROP POLICY IF EXISTS "Authenticated users can view all orders" ON public.orders; +CREATE POLICY "Admins can view orders" + ON public.orders FOR SELECT + TO authenticated + USING (public.has_role(auth.uid(), 'admin')); + +-- Customers: admin-only +DROP POLICY IF EXISTS "Authenticated users can manage customers" ON public.customers; +DROP POLICY IF EXISTS "Authenticated users can view customers" ON public.customers; +CREATE POLICY "Admins can view customers" + ON public.customers FOR SELECT + TO authenticated + USING (public.has_role(auth.uid(), 'admin')); +CREATE POLICY "Admins can manage customers" + ON public.customers FOR ALL + TO authenticated + USING (public.has_role(auth.uid(), 'admin')) + WITH CHECK (public.has_role(auth.uid(), 'admin')); + +-- Payments: admin-only reads +DROP POLICY IF EXISTS "Authenticated users can view payments" ON public.payments; +CREATE POLICY "Admins can view payments" + ON public.payments FOR SELECT + TO authenticated + USING (public.has_role(auth.uid(), 'admin')); diff --git a/supabase/migrations/20260711135310_537e97ea-5e65-4280-a0e4-c88bef579b27.sql b/supabase/migrations/20260711135310_537e97ea-5e65-4280-a0e4-c88bef579b27.sql new file mode 100644 index 0000000..7b0e0fa --- /dev/null +++ b/supabase/migrations/20260711135310_537e97ea-5e65-4280-a0e4-c88bef579b27.sql @@ -0,0 +1,3 @@ + +DROP POLICY IF EXISTS "Service role can insert orders" ON public.orders; +DROP POLICY IF EXISTS "Service can insert payments" ON public.payments;