diff --git a/src/App.tsx b/src/App.tsx index 6162613..acc0c71 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Index from "./pages/Index"; import NotFound from "./pages/NotFound"; +import ResetPassword from "./pages/ResetPassword"; import { useCartSync } from "./hooks/useCartSync"; const queryClient = new QueryClient(); @@ -16,6 +17,7 @@ function AppContent() { } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/components/AuthDialog.tsx b/src/components/AuthDialog.tsx index 179d7ae..eb7cd52 100644 --- a/src/components/AuthDialog.tsx +++ b/src/components/AuthDialog.tsx @@ -17,6 +17,12 @@ const authSchema = z.object({ password: z.string().min(6, { message: "Lösenordet måste vara minst 6 tecken" }).max(100), }); +const emailSchema = z.object({ + email: z.string().trim().email({ message: "Ogiltig e-postadress" }).max(255), +}); + +type AuthMode = "login" | "signup" | "forgot"; + interface AuthDialogProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -24,16 +30,54 @@ interface AuthDialogProps { onModeChange: (mode: "login" | "signup") => void; } -const AuthDialog = ({ open, onOpenChange, mode, onModeChange }: AuthDialogProps) => { +const AuthDialog = ({ open, onOpenChange, mode: initialMode, onModeChange }: AuthDialogProps) => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); - const { signIn, signUp } = useAuth(); + const [internalMode, setInternalMode] = useState(initialMode); + const { signIn, signUp, resetPassword } = useAuth(); const { toast } = useToast(); + // Sync internal mode with external mode prop + const mode = internalMode === "forgot" ? "forgot" : initialMode; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if (internalMode === "forgot") { + const validation = emailSchema.safeParse({ email }); + if (!validation.success) { + toast({ + title: "Fel", + description: validation.error.errors[0].message, + variant: "destructive", + }); + return; + } + + setIsSubmitting(true); + try { + const { error } = await resetPassword(email); + if (error) { + toast({ + title: "Kunde inte skicka återställningslänk", + description: error.message, + variant: "destructive", + }); + } else { + toast({ + title: "E-post skickad!", + description: "Kolla din inkorg för att återställa ditt lösenord.", + }); + onOpenChange(false); + resetForm(); + } + } finally { + setIsSubmitting(false); + } + return; + } + const validation = authSchema.safeParse({ email, password }); if (!validation.success) { toast({ @@ -47,7 +91,7 @@ const AuthDialog = ({ open, onOpenChange, mode, onModeChange }: AuthDialogProps) setIsSubmitting(true); try { - if (mode === "signup") { + if (initialMode === "signup") { const { error } = await signUp(email, password); if (error) { toast({ @@ -88,14 +132,33 @@ const AuthDialog = ({ open, onOpenChange, mode, onModeChange }: AuthDialogProps) const resetForm = () => { setEmail(""); setPassword(""); + setInternalMode(initialMode); + }; + + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen) { + resetForm(); + } + onOpenChange(newOpen); + }; + + const getTitle = () => { + if (internalMode === "forgot") return "Glömt lösenord"; + return initialMode === "signup" ? "Skapa konto" : "Logga in"; + }; + + const getSubmitText = () => { + if (isSubmitting) return "Laddar..."; + if (internalMode === "forgot") return "Skicka återställningslänk"; + return initialMode === "signup" ? "Skapa konto" : "Logga in"; }; return ( - + - {mode === "signup" ? "Skapa konto" : "Logga in"} + {getTitle()} @@ -112,54 +175,83 @@ const AuthDialog = ({ open, onOpenChange, mode, onModeChange }: AuthDialogProps) /> -
- - setPassword(e.target.value)} - placeholder="••••••••" - required - /> -
+ {internalMode !== "forgot" && ( +
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + /> +
+ )} -
- {mode === "signup" ? ( +
+ {internalMode === "forgot" ? (

- Har du redan ett konto?{" "}

) : ( -

- Har du inget konto?{" "} - -

+ <> + {initialMode === "login" && ( +

+ +

+ )} + {initialMode === "signup" ? ( +

+ Har du redan ett konto?{" "} + +

+ ) : ( +

+ Har du inget konto?{" "} + +

+ )} + )}
diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index b574bc9..7b5045b 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -51,6 +51,20 @@ export function useAuth() { return { error }; }; + const resetPassword = async (email: string) => { + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: `${window.location.origin}/reset-password`, + }); + return { error }; + }; + + const updatePassword = async (newPassword: string) => { + const { error } = await supabase.auth.updateUser({ + password: newPassword, + }); + return { error }; + }; + return { user, session, @@ -58,5 +72,7 @@ export function useAuth() { signUp, signIn, signOut, + resetPassword, + updatePassword, }; } diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..5fade6d --- /dev/null +++ b/src/pages/ResetPassword.tsx @@ -0,0 +1,139 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useAuth } from "@/hooks/useAuth"; +import { useToast } from "@/hooks/use-toast"; + +const passwordSchema = z.object({ + password: z.string().min(6, { message: "Lösenordet måste vara minst 6 tecken" }).max(100), + confirmPassword: z.string(), +}).refine((data) => data.password === data.confirmPassword, { + message: "Lösenorden matchar inte", + path: ["confirmPassword"], +}); + +const ResetPassword = () => { + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isReady, setIsReady] = useState(false); + const { updatePassword, session } = useAuth(); + const { toast } = useToast(); + const navigate = useNavigate(); + + useEffect(() => { + // Wait for session to be available (user clicked email link) + if (session) { + setIsReady(true); + } + }, [session]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const validation = passwordSchema.safeParse({ password, confirmPassword }); + if (!validation.success) { + toast({ + title: "Fel", + description: validation.error.errors[0].message, + variant: "destructive", + }); + return; + } + + setIsSubmitting(true); + + try { + const { error } = await updatePassword(password); + if (error) { + toast({ + title: "Kunde inte uppdatera lösenord", + description: error.message, + variant: "destructive", + }); + } else { + toast({ + title: "Lösenord uppdaterat!", + description: "Du kan nu logga in med ditt nya lösenord.", + }); + navigate("/"); + } + } finally { + setIsSubmitting(false); + } + }; + + if (!isReady) { + return ( +
+
+

Laddar...

+

+ Väntar på verifiering av din återställningslänk. +

+
+
+ ); + } + + return ( +
+
+
+

+ Välj nytt lösenord +

+ +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + /> +
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="••••••••" + required + /> +
+ + +
+ +
+ +
+
+
+
+ ); +}; + +export default ResetPassword;