mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 19:01:16 +00:00
added auth comments
This commit is contained in:
parent
5a94b63b23
commit
6636083e71
8 changed files with 159 additions and 12 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"prisma.pinToPrisma6": true
|
||||
"prisma.pinToPrisma6": true,
|
||||
"docwriter.style": "JSDoc"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,13 +68,24 @@ export const useAuth = () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticates the user and initializes the session in global state.
|
||||
* @param email - User's email address
|
||||
* @param password - Plain text password
|
||||
* @param remember - Boolean to determine session persistence
|
||||
* @returns {Promise<boolean>} - True if login and redirect were successful
|
||||
*/
|
||||
const loginUser = async (email: string, password: string, remember: boolean) => {
|
||||
//TODO: Implement 'remember me' persistence logic
|
||||
|
||||
setErrors({});
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// 1. Validate inputs
|
||||
userLoginSchema.parse({ email, password });
|
||||
|
||||
// 2. Attempt login via Supabase
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
|
|
@ -86,16 +97,18 @@ export const useAuth = () => {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Update Zustand store
|
||||
// 3. Update global store
|
||||
// The User-Store must manually update for Navbar etc.
|
||||
setUser(data.user);
|
||||
|
||||
toast.success("Erfolgreich eingeloggt 🎉");
|
||||
|
||||
// Navigate to dashboard (no refresh needed - middleware handles it)
|
||||
// 4. Navigation
|
||||
router.push("/dashboard");
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
// Zod Validation Error (for UI)
|
||||
if (err instanceof ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.issues.forEach((issue) => {
|
||||
|
|
@ -105,9 +118,11 @@ export const useAuth = () => {
|
|||
setErrors(fieldErrors);
|
||||
return false;
|
||||
}
|
||||
// Catch-all for Network Errors or Supbase Auth Failure
|
||||
toast.error("Ein unerwarteter Fehler ist aufgetreten.");
|
||||
return false;
|
||||
} finally {
|
||||
// Reset Loading to free the Login Button
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from "@/src/modules/employee/actions/employee-actions";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Step 1: Personal + account data validation
|
||||
const step1Schema = z
|
||||
.object({
|
||||
firstName: z.string().min(2, "Vorname ist zu kurz"),
|
||||
|
|
@ -35,6 +36,7 @@ const step1Schema = z
|
|||
path: ["passwordConfirm"],
|
||||
});
|
||||
|
||||
// Step 2: Additional profile data
|
||||
const step2Schema = z.object({
|
||||
gender: z
|
||||
.enum(["Men", "Woman", "Various"])
|
||||
|
|
@ -44,10 +46,8 @@ const step2Schema = z.object({
|
|||
});
|
||||
|
||||
const EmployeeOnboardingView = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const token = params?.onboardingToken as string;
|
||||
|
||||
// ------------- States -------------
|
||||
// Multi-step flow + UI states
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tokenLoading, setTokenLoading] = useState(true);
|
||||
|
|
@ -55,6 +55,7 @@ const EmployeeOnboardingView = () => {
|
|||
const [employeeEmail, setEmployeeEmail] = useState("");
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Form states
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const [tel, setTel] = useState("");
|
||||
|
|
@ -65,7 +66,18 @@ const EmployeeOnboardingView = () => {
|
|||
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
||||
|
||||
// ------------- Hooks -------------
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const token = params?.onboardingToken as string;
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Validates onboarding token on page load
|
||||
*
|
||||
* - Fetches employee data via token
|
||||
* - Prevents access if token is invalid/expired
|
||||
*/
|
||||
const checkToken = async () => {
|
||||
if (!token) {
|
||||
setTokenError("Kein Onboarding-Token gefunden");
|
||||
|
|
@ -83,6 +95,11 @@ const EmployeeOnboardingView = () => {
|
|||
checkToken();
|
||||
}, [token]);
|
||||
|
||||
/**
|
||||
* Handles step 1 validation and navigation
|
||||
*
|
||||
* @param {React.FormEvent} e - Form submit event
|
||||
*/
|
||||
const handleNext = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
|
@ -101,6 +118,16 @@ const EmployeeOnboardingView = () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Final onboarding submission
|
||||
*
|
||||
* - Validates step 2 data
|
||||
* - Uploads avatar (optional)
|
||||
* - Completes onboarding via backend
|
||||
*
|
||||
* @param {React.FormEvent} e - Form submit event
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
|
@ -120,6 +147,7 @@ const EmployeeOnboardingView = () => {
|
|||
|
||||
setLoading(true);
|
||||
|
||||
// Optional avatar upload before completing onboarding
|
||||
let pbSrc: string | undefined;
|
||||
if (avatarFile) {
|
||||
try {
|
||||
|
|
@ -156,6 +184,7 @@ const EmployeeOnboardingView = () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Loading state while validating token
|
||||
if (tokenLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen justify-center items-center">
|
||||
|
|
@ -164,6 +193,7 @@ const EmployeeOnboardingView = () => {
|
|||
);
|
||||
}
|
||||
|
||||
// Invalid or expired token
|
||||
if (tokenError) {
|
||||
return (
|
||||
<div className="flex min-h-screen justify-center items-center flex-col gap-4 px-4">
|
||||
|
|
@ -177,6 +207,7 @@ const EmployeeOnboardingView = () => {
|
|||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar with onboarding steps */}
|
||||
<div className="hidden lg:block lg:w-[40%]">
|
||||
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
|
||||
<AuthSidebarComponent
|
||||
|
|
@ -190,11 +221,13 @@ const EmployeeOnboardingView = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main onboarding form */}
|
||||
<div className="w-full lg:w-[60%] lg:ml-auto flex justify-center items-center min-h-screen px-4">
|
||||
<div className="w-full max-w-lg flex flex-col gap-4 z-20">
|
||||
<h1 className="text-2xl font-extrabold text-blue mb-2 mt-8">Herzlich Willkommen!</h1>
|
||||
<p className="text-gray-500 mb-6">{employeeEmail}</p>
|
||||
|
||||
{/* Step 1: Personal data + login data*/}
|
||||
{step === 1 && (
|
||||
<form onSubmit={handleNext} className="space-y-5">
|
||||
<div className="flex gap-4">
|
||||
|
|
@ -249,6 +282,7 @@ const EmployeeOnboardingView = () => {
|
|||
</form>
|
||||
)}
|
||||
|
||||
{/* Step 2: Addtional (optinal) Employee Data */}
|
||||
{step === 2 && (
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div className="flex flex-col items-center">
|
||||
|
|
|
|||
|
|
@ -8,13 +8,23 @@ import { Spinner } from "@/src/components/ui/spinner";
|
|||
import { createClient } from "@/src/lib/supabase/client";
|
||||
|
||||
const ForgotPasswordView = () => {
|
||||
// ------------- States -------------
|
||||
const [email, setEmail] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [emailSent, setEmailSent] = useState(false);
|
||||
|
||||
// ------------- Hooks -------------
|
||||
const router = useRouter();
|
||||
const supabase = createClient();
|
||||
|
||||
/**
|
||||
* Sends a password reset email via Supabase
|
||||
* Validates input and updates error/success state
|
||||
*
|
||||
* @param {React.FormEvent} e - Form submit event
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleReset = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setEmailError("");
|
||||
|
|
@ -27,6 +37,7 @@ const ForgotPasswordView = () => {
|
|||
}
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
// Redirects user to custom reset-password page after clicking email link
|
||||
redirectTo: `${window.location.origin}/reset-password`,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,12 +12,19 @@ import { Spinner } from "@/src/components/ui/spinner";
|
|||
import { useAuth } from "@/src/hooks/useAuth";
|
||||
|
||||
const LoginView = () => {
|
||||
const { loginUser, errors, loading } = useAuth();
|
||||
// ------------- States -------------
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [remember, setRemember] = useState(false);
|
||||
|
||||
// ------------- Hooks -------------
|
||||
const { loginUser, errors, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* Login of a Manager/User/Employee using loginUser Function from useAuth
|
||||
* @param e - React Form Event (like klicking)
|
||||
*/
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const success = await loginUser(email, password, remember);
|
||||
|
|
@ -92,6 +99,7 @@ const LoginView = () => {
|
|||
|
||||
<hr className="mt-6 border-blue" />
|
||||
|
||||
{/* Social Login Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mt-4">
|
||||
<AuthButton Icon={FaGoogle} label="Login mit Google" />
|
||||
<AuthButton Icon={FaFacebook} label="Login mit Facebook" />
|
||||
|
|
|
|||
|
|
@ -17,15 +17,28 @@ import { Spinner } from "@/src/components/ui/spinner";
|
|||
import { useRouter } from "next/navigation";
|
||||
|
||||
const RegisterAdmin = () => {
|
||||
// ------------- States -------------
|
||||
// Controls multi-step registration flow
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
// ------------- Hooks -------------
|
||||
const { registerAdmin, loading, errors, setErrors } = useAuth();
|
||||
const router = useRouter();
|
||||
const formData = useAuthFormStore();
|
||||
|
||||
// Generic handler to update form state
|
||||
const handleChange = (field: string, value: string) => {
|
||||
formData.setField(field, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles step 1 and 2 validation before moving to step 3
|
||||
*
|
||||
* Uses Zod schema to validate user input
|
||||
* Maps validation errors to UI-friendly error-object.
|
||||
*
|
||||
* @param {React.FormEvent} e - Form submit event
|
||||
*/
|
||||
const handleNext = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
|
@ -48,6 +61,7 @@ const RegisterAdmin = () => {
|
|||
setStep(3);
|
||||
}
|
||||
} catch (err) {
|
||||
// Transform Zod erros into field-based error object
|
||||
if (err instanceof ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.issues.forEach((issue) => {
|
||||
|
|
@ -61,6 +75,15 @@ const RegisterAdmin = () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Final step: validates company data and registers admin
|
||||
*
|
||||
* - Validates step 3 (company info)
|
||||
* - Sends full dataset to backend
|
||||
* - Resets form and redirects on success
|
||||
*
|
||||
* @param {React.FormEvent} e - Form submit event
|
||||
*/
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
|
@ -92,6 +115,7 @@ const RegisterAdmin = () => {
|
|||
});
|
||||
|
||||
if (success) {
|
||||
// Clear persisted form state
|
||||
formData.resetForm();
|
||||
router.push("/login");
|
||||
}
|
||||
|
|
@ -111,6 +135,7 @@ const RegisterAdmin = () => {
|
|||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar with step indicator */}
|
||||
<div className="hidden lg:block lg:w-[40%]">
|
||||
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
|
||||
<AuthSidebarComponent
|
||||
|
|
@ -124,10 +149,12 @@ const RegisterAdmin = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main form */}
|
||||
<div className="w-full lg:w-[60%] lg:ml-auto flex justify-center items-center min-h-screen px-4">
|
||||
<div className="w-full max-w-lg flex flex-col gap-4 z-20">
|
||||
<h1 className="text-2xl font-extrabold text-blue mb-8 mt-8 ">Sign up for free</h1>
|
||||
|
||||
{/* Step 1: Personal + login data */}
|
||||
{step === 1 && (
|
||||
<form onSubmit={handleNext} className="space-y-5">
|
||||
<InputComponent
|
||||
|
|
@ -169,7 +196,7 @@ const RegisterAdmin = () => {
|
|||
isLabel
|
||||
label="Passwort bestätigen"
|
||||
type="password"
|
||||
id="register-admin-passwordConform"
|
||||
id="register-admin-passwordConfirm"
|
||||
value={formData.passwordConfirm}
|
||||
onChange={(e) => handleChange("passwordConfirm", e.target.value)}
|
||||
error={errors.passwordConfirm}
|
||||
|
|
@ -181,6 +208,7 @@ const RegisterAdmin = () => {
|
|||
</form>
|
||||
)}
|
||||
|
||||
{/* Step 2: Private address */}
|
||||
{step === 2 && (
|
||||
<form onSubmit={handleNext} className="space-y-5">
|
||||
<AddressAutocompleteComponent
|
||||
|
|
@ -207,6 +235,7 @@ const RegisterAdmin = () => {
|
|||
</form>
|
||||
)}
|
||||
|
||||
{/* Step 3: Company data */}
|
||||
{step === 3 && (
|
||||
<form onSubmit={handleRegister} className="space-y-5">
|
||||
<InputComponent
|
||||
|
|
|
|||
|
|
@ -15,15 +15,28 @@ import { useRouter } from "next/navigation";
|
|||
import { useAuth } from "@/src/hooks/useAuth";
|
||||
|
||||
const RegisterUserView = () => {
|
||||
// ------------- States -------------
|
||||
// Controls multi-step registration flow
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
// ------------- Hooks -------------
|
||||
const { registerUser, loading, errors, setErrors } = useAuth();
|
||||
const router = useRouter();
|
||||
const formData = useAuthFormStore();
|
||||
|
||||
// Generic handler to update form state
|
||||
const handleChange = (field: string, value: string) => {
|
||||
formData.setField(field, value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles step 1 validation before moving to step 2
|
||||
*
|
||||
* Uses Zod schema to validate user input
|
||||
* Maps validation errors to UI-friendly error-object.
|
||||
*
|
||||
* @param {React.FormEvent} e - Form submit event
|
||||
*/
|
||||
const handleNext = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
|
@ -38,6 +51,7 @@ const RegisterUserView = () => {
|
|||
setStep(2);
|
||||
}
|
||||
} catch (err) {
|
||||
// Transform Zod erros into field-based error object
|
||||
if (err instanceof ZodError) {
|
||||
const fieldErrors: Record<string, string> = {};
|
||||
err.issues.forEach((issue) => {
|
||||
|
|
@ -51,6 +65,15 @@ const RegisterUserView = () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Final registration handler
|
||||
*
|
||||
* - Sends full form data to backend
|
||||
* - Resets form on success
|
||||
* - Redirects user to login
|
||||
*
|
||||
* @param {React.FormEvent} e - Form submit event
|
||||
*/
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const success = await registerUser({
|
||||
|
|
@ -66,6 +89,7 @@ const RegisterUserView = () => {
|
|||
});
|
||||
|
||||
if (success) {
|
||||
// Clear persisted form state
|
||||
formData.resetForm();
|
||||
router.push("/login");
|
||||
}
|
||||
|
|
@ -73,7 +97,7 @@ const RegisterUserView = () => {
|
|||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Sidebar */}
|
||||
{/* Sidebar with step indicator */}
|
||||
<div className="hidden lg:block lg:w-[40%]">
|
||||
<div className="hidden lg:block fixed left-0 top-0 h-screen w-[40%] p-2 z-10">
|
||||
<AuthSidebarComponent
|
||||
|
|
@ -87,10 +111,12 @@ const RegisterUserView = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main registration form */}
|
||||
<div className="w-full lg:w-[60%] lg:ml-auto flex justify-center items-center min-h-screen px-4">
|
||||
<div className="w-full max-w-lg flex flex-col gap-4 z-20">
|
||||
<h1 className="text-2xl font-extrabold text-blue mb-8 mt-8 ">Sign up for free</h1>
|
||||
|
||||
{/* Step 1: Basic account data */}
|
||||
{step === 1 && (
|
||||
<form onSubmit={handleNext} className="space-y-5">
|
||||
<InputComponent
|
||||
|
|
@ -125,7 +151,7 @@ const RegisterUserView = () => {
|
|||
isLabel
|
||||
label="Passwort bestätigen"
|
||||
type="password"
|
||||
id="register-user-passwordConform"
|
||||
id="register-user-passwordConfirm"
|
||||
value={formData.passwordConfirm}
|
||||
onChange={(e) => handleChange("passwordConfirm", e.target.value)}
|
||||
error={errors.passwordConfirm}
|
||||
|
|
@ -137,6 +163,7 @@ const RegisterUserView = () => {
|
|||
</form>
|
||||
)}
|
||||
|
||||
{/* Step 2: Additional user details */}
|
||||
{step === 2 && (
|
||||
<form onSubmit={handleRegister} className="space-y-5">
|
||||
<InputComponent
|
||||
|
|
@ -144,7 +171,7 @@ const RegisterUserView = () => {
|
|||
label="Geburtsdatum"
|
||||
type="date"
|
||||
id="register-user-birthday"
|
||||
max={new Date().toISOString().split("T")[0]}
|
||||
max={new Date().toISOString().split("T")[0]} // Prevent future dates
|
||||
value={formData.birthday}
|
||||
onChange={(e) => handleChange("birthday", e.target.value)}
|
||||
error={errors.birthday}
|
||||
|
|
@ -158,6 +185,7 @@ const RegisterUserView = () => {
|
|||
onPlzChange={(v) => handleChange("plz", v)}
|
||||
onPlaceChange={(v) => handleChange("place", v)}
|
||||
onSelect={({ street, plz, place }) => {
|
||||
// Autofill address fields from selection
|
||||
handleChange("street", street);
|
||||
handleChange("plz", plz);
|
||||
handleChange("place", place);
|
||||
|
|
@ -182,6 +210,7 @@ const RegisterUserView = () => {
|
|||
|
||||
<hr className="mt-6 border-blue" />
|
||||
|
||||
{/* Social login options */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 mt-4">
|
||||
<AuthButton Icon={FaGoogle} label="Login mit Google" />
|
||||
<AuthButton Icon={FaFacebook} label="Login mit Facebook" />
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@ import { Spinner } from "@/src/components/ui/spinner";
|
|||
import { createClient } from "@/src/lib/supabase/client";
|
||||
|
||||
const ResetPasswordView = () => {
|
||||
// ------------- States -------------
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
const [confirmPasswordError, setConfirmPasswordError] = useState("");
|
||||
const [resetSuccess, setResetSuccess] = useState(false);
|
||||
|
||||
// ------------- Hooks -------------
|
||||
const router = useRouter();
|
||||
const supabase = createClient();
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure user is authenticated via reset link session
|
||||
// If not, redirect to login (invalid or expired link)
|
||||
supabase.auth.getUser().then(({ data }) => {
|
||||
if (!data.user) {
|
||||
router.replace("/login");
|
||||
|
|
@ -25,6 +30,17 @@ const ResetPasswordView = () => {
|
|||
});
|
||||
}, [router, supabase]);
|
||||
|
||||
/**
|
||||
* Handles password reset submission
|
||||
*
|
||||
* - Validation password rules
|
||||
* - Updates password via Supabase
|
||||
* - Signs user out after success (security best practice)
|
||||
* - Redirects to login after short delay
|
||||
*
|
||||
* @param {React.FormEvent} e - Form submit event
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleResetPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
@ -32,6 +48,7 @@ const ResetPasswordView = () => {
|
|||
setPasswordError("");
|
||||
setConfirmPasswordError("");
|
||||
|
||||
// Basic Password validation
|
||||
if (password.length < 8) {
|
||||
setPasswordError("Mindestens 8 Zeichen");
|
||||
setLoading(false);
|
||||
|
|
@ -44,6 +61,7 @@ const ResetPasswordView = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update password for the currently authenticated user
|
||||
const { error } = await supabase.auth.updateUser({ password });
|
||||
|
||||
if (error) {
|
||||
|
|
@ -54,12 +72,14 @@ const ResetPasswordView = () => {
|
|||
|
||||
setResetSuccess(true);
|
||||
|
||||
// Force logout after password change to refresh session
|
||||
try {
|
||||
await supabase.auth.signOut();
|
||||
} catch (signOutError) {
|
||||
console.error("Sign out error:", signOutError);
|
||||
}
|
||||
|
||||
// Redirect to login after success feedback
|
||||
setTimeout(() => {
|
||||
router.push("/login");
|
||||
}, 2500);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue